Психология читабельности кода

https://medium.com/@egonelbre/psychology-of-code-readability-d23b1ff1258a
  • Перевод
Всё, что написано ниже, ни разу не претендует на абсолютную истину, но всё же представляет собой некоторую модель, помогающую лично мне находить способы писать немного лучший код.

Каждый программист старается писать хороший код. Читабельность — один из главных признаков такого кода. О ней написано достаточно много книг, но всё же в теме есть пробелы. Например, те самые книги сфокусированы больше на советах КАК написать читабельный код, а не на причинах того, почему один код является хорошо читабельным, а другой — нет. Книга говорит нам «используйте подходящие названия переменных» — но что делает одно название более подходящим, чем другое? Работает ли это для всех примеров подобного кода? Работает ли это для всех программистов, которым попадётся на глаза этот код? Как раз о последнем я и хотел бы поговорить чуть детальнее. Давайте погрузимся немного в человеческую психику. Наш мозг — главный наш инструмент, хорошо бы изучить специфику его работы.

Психологическое основание


Каждый программист знает, что возможности нашего мозга не безграничны. Есть ограничение на количество вещей, о которых мы можем думать. Это наш рабочий лимит памяти. Есть старый миф о том, что человек может держать в памяти одновременно 7±2 объектов. Это называется "Магическое число семь" и оно на самом деле не очень точное. Последние исследования говорят о числе 4±1, а то и меньше. В любом случае — количество идей, которые мы можем держать одновременно в голове, весьма ограниченно.

Некоторые люди скажут, что способны легко оперировать одновременно более, чем четырьмя объектами в памяти. Это так: к счастью, есть ещё один процесс, который постоянно происходит в нашей голове — это группировка. Мы объединяем схожие мелкие сущности в чуть большие и оперируем уже ими. Вспомните, как вы называете даты или номера телефонов — не по одной цифре, а группами по две, три или четыре. При этом каждая группа цифр является самостоятельной сущностью. Более того, все цифры вместе формируют «дату» или «номер телефона» — тоже отдельные сущности.

image

Из этих групп мы строим нашу долговременную память. Я представляю её как большую паутину из таких вот группок и их последовательностей.

image

Исходя из данной картинки, вам может показаться, что перемещение из одной части памяти в другую происходит достаточно медленно. И это действительно так. В науке проектирования пользовательских интерфейсов есть концепция «единого фокуса внимания». Название говорит само за себя — мы можем фокусироваться в каждый момент времени только на чём-то одном. Более того, кроме фокуса, есть ещё и «локус» — ограниченность внимания в пространстве.

Вы можете подумать, что это то же самое, что и упомянутый выше рабочий лимит памяти, но есть важное отличие. Рабочий лимит памяти говорит о том, сколько всего сущностей мы можем держать в памяти. Фокус и локус внимания говорят о том, что для выполнение какой-то полезной мыслительной работы данные сущности ещё и должны «находиться рядом», быть чем-то связанными.

О фокусе и локусе внимания важно знать, поскольку переключение между идеями — весьма затратный процесс. И ещё более затратным он становится, когда нам на ходу нужно осмысливать незнакомые сущности и группировать их. К счастью, это работает и в обратную сторону — чем что-то нам лучше знакомо, тем меньше времени требуется, чтобы переключить на него внимание и осмыслить. Это называется опытом.

Также мы по-разному запоминаем вещи в зависимости от контекста. Однажды проводился следующий эксперимент: группе дайверов читали набор слов, а затем, через некоторое время, просили их воспроизвести. И первоначальное чтение, и воспроизведение проводилось как на суше, так и в воде. Лучше всего обстояло дело со случаем, когда слова и читались и воспроизводились на суше. Но на втором месте неожиданно оказалась комбинация с чтением и воспроизведением в воде. Люди запоминали слова в контексте окружающей обстановки и аналогичная окружающая обстановка помогала лучше вспоминать.

Из контекстов и сгруппированных по ячейкам памяти сущностей мы и строим ментальные представления и ментальные модели. Ментальные модели играют ключевую роль для нашей способности находить решения проблем. Для одной и той же проблемы можно построить разные ментальные модели, и каждая будет иметь свои плюсы и минусы. Есть и главная проблема в их построении и применении: наш мозг. О, у нашего мозга есть целая куча разных недостатков.

Во-первых, ему сложновато работать с абстракциями. Когда некоторые сущности кажутся схожыми, они располагаются в мозгу «рядом», являются связанными. Это приводит к тому, что мозг иногда ошибается какую из них следует извлечь и использовать в каждом конкретном случае. Пример: путаница между l и 1, 0 и О. Ещё один пример — двусмысленность. «Ключ» — это мы сейчас о предмете для открывания замков, построении стаи птиц или инструменте для работы с гайками?

Неуверенность в правильности найденной абстракции замедляет процесс мышления. На какую-то долю секунды — но и этого может хватить для потери фокуса внимания. Нас вообще много чего может сбить, но, если крупные отвлекающие факторы мы способны понять, локализовать и отфильтровать, то «всякую мелочь» можем и не осознавать. Если кто-то будет называть случайные числа, пока вы что-то считаете — считать станет существенно сложнее. Это может происходить и с визуальными факторами: если на экране есть несколько важных в текущей ситуации объектов и пару десятков не важных — вам будет сложнее выделить и осознать лишь важное.

Всё вышеуказанное создаёт когнитивную нагрузку. Это количество ментальных усилий, которые необходимы для решения некоторой задачи. Наша «операционная мощность» падает по ходу работы и возрастает после отдыха. Если вы не даёте своей голове осознанного отдыха — природу вы всё-равно не обманете и через какое-то время ваш мозг начнёт «витать в облаках».

Давайте теперь перейдём ближе к делу и подумаем, как всё это касается написание хорошего кода. Дальше будет несколько рекомендаций, эмпирических правил и парадигм, которые могут так или иначе вам помочь с точки зрения психологии. Это, конечно, не фундаментальное руководство, но, я надеюсь, вы уловите основные идеи и дальше сможете их применять для оценки других известных вам правил, советов и парадигм.

Именование сущностей


Давайте взглянем на простенький цикл for:

  • A. for(i=0 to N)
  • B. for(theElementIndex=0 to theNumberOfElementsInTheList)

Какой вариант нравится вам больше? Большинство программистов порекомендуют вариант А. Почему? А потому, что вариант B использует слишком длинные имена переменных, что мешает нам с одного взгляда увидеть единый (и хорошо знакомый) паттерн. Кроме того, в данном случае столь длинные имена и не помогают создать более качественный контекст, они просто добавляют шум.

Теперь давайте посмотрим на различные способы формирования пространств имён (это могут быть пакеты, модули или что-то ещё в вашем языке программирования):

  • A. strings.IndexOf(x, y)
  • B. s.IndexOf(x, y)
  • C. std.utils.strings.IndexOf(x, y)
  • D. IndexOf(x, y)

Вариант В плох, поскольку «s» — слишком короткое название и не помогает нам понять, что «это, наверное, строка».

Вариант С плох, поскольку std.utils.strings — слишком длинное название, мы и так поняли, что это строка, не нужно каждый раз напоминать о том, где она находится.
Вариант D плох, поскольку без пространств имён мы вообще не очень хорошо понимаем, что за функцию вызываем, откуда она и над какими объектами будет работать.

Важно заметить, что если уж речь в коде зашла о строках, то логичным будет предположить, что вызов IndexOf для строки выполняет какую-то работу именно на строке. В таком случае, даже упоминание пространства имён «strings» будет излишним, как, например, операция сложения на целых числах более понятна в виде a + b, а не в виде int16.Add(a, b).

Состояние переменной


Некоторые парадигмы программирования говорят, что любая модификация переменной является плохой вещью, ведь «становится трудно понять, что и когда изменилось». Но давайте посмотрим вот на эти примеры:

// A.
func foo() (int, int) {
    sum, sumOfSquares := 0, 0
    for _, v := range values {
        sum += v
        sumOfSquares += v * v
    }
    return sum, sumOfSquares
}

// B.
func GCD(a, b int) int {
      for b != 0 {
              a, b = b, a % b
      }
      return a
}

// C.
func GCD(a, b int) int {
    if b == 0 {
        return a
    }
    return GCD(b, a % b)
}

Здесь первую функцию (foo), наверное, легче всего понять. Почему? Потому, что проблема не в модификации переменных, а в том, как именно они модифицируются. Пример А не содержит никаких сложных вычислений, в отличии от B и С.

// D.
sum = sum + v.x
sum = sum + v.y
sum = sum + v.z
sum = sum + v.w

// E.
sum1 = v.x
sum2 := sum1 + v.y
sum3 := sum2 + v.z
sum4 := sum3 + v.w

Вот ещё один пример кода, где версия с модификацией значения переменной (D) читается проще. Вариант Е не модифицирует существующие переменные, но добавляет 3 новых сущности для описания той же идеи. Больше шума — сложнее понимание.

Идиомы


Давайте посмотрим ещё на несколько циклов:

  • A. for(i = 0; i < N; i++)
  • B. for(i = 0; N > i; i++)
  • D. for(i = 0; i <= N-1; i += 1)
  • C. for(i = 0; N-1 >= i; i += 1)

Насколько долго у вас заняло понять, что делает каждый из них? Бьюсь об заклад, вариант А вы восприняли на лету. Остальные три варианта пришлось читать и понимать. Главная причина — опыт. Вариант А у многих программистов лежит в отдельной, быстро-доступной ячейке памяти. Остальные три — нет. Для них нужно строить в голове новые временные модели.

Но для новичка все четыре варианта будут выглядеть одинаково — их сложность действительно примерно равна и ни один из них не покажется человеку «со стороны» лучше другого. Опытный программист с первого взгляда скажет «а, ну это проход по массиву». Новичок же расскажет о том, что «здесь мы обнуляем переменную i, потом сравниваем её с N, выполняем код внутри тела цикла, увеличиваем i и снова сравниваем и т.д.».

Вариант А — это «идиоматический способ» написания цикла. Он не лучше остальных с точки зрения вычислительной сложности, но значительно лучше с точки зрения читабельности кода, ведь он входит в «базовый словарь профессии».

Большинство языков программирования имеют идиоматический способ написания тех или иных вещей. Есть классические документы и книги, типа APL idioms, C++ idioms а также более высокоуровневые вещи вроде паттернов Банды Четырёх. Используя идиомы из подобных классических книг, мы можем строить более сложные программы, отдельные куски которых будут понятны остальным программистам (ведь они, наверное, читали те же книги).

У всего этого есть и негативный аспект. Чем больше идиом мы используем, тем больший вокабуляр требуется запоминать для их понимания. Динамические языки подвержены данной проблеме больше остальных — программисты чувствуют возможность изобрести новые гибкие идиомы для хитрого решения своей текущей проблемы (и это даже работает), но каждый следующий читатель данного кода будет вынужден замедляться, пытаясь понять неожиданные подходы автора.

Консистентность


Хорошим примером консистентности могут быть названия сущностей типа «модель» и «контроллер». Выучив однажды что это и как они связаны друг с другом, вы навсегда приобретаете в своей голове ценную пару идиом. Теперь в любом коде, увидев класс со словом Model или Controller в названии, вы будете понимать, для чего он создан и с чем связан.

Такие вещи как фреймворки или игровые движки всегда пытаются действовать подобным образом: дать нам понимание базовых сущностей, связей между ними и способов манипуляции ими. Выучив структуру одного проекта на каком-то фреймворке или движке, программист может очень быстро вникнуть в суть другого проекта на нём же.

Важным фактором здесь является и консистентность оформления кода. Чем более в коде консистентны имена переменных, классов, методов, форматирование кода, подходы к решению одинаковых проблем в разных местах кодовой базы — тем легче читатель понимает проект, тем быстрее начинает «доверять» ему.

Неопределённость


Неопределённость может замедлить как написание, так и понимание кода. В качестве примера можно рассмотреть двусмысленность. Например, вот такой код:

[1,2,3].filter(v => v >= 2)

при всей своей простоте всё-же оставляет открытым вопрос, что же будет получено в итоге «2 и 3» или «1»? То есть мы здесь «фильтруем» или «отфильтровываем»? Скорее всего вы быстро найдёте ответ в документации вашей платформы или используемой библиотеки — но вам придётся отвлечься, а потом ещё и запомнить найденную информацию. Правда, было бы лучше, если бы название и синтаксис говорили сами за себя? Значительно лучше подошли бы названия функций типа select, discard или keep.

Мы также можем по-разному понимать значение той или иной сущности. Например, функция GetUser(string) одними людьми может быть воспринята как поиск пользователя по имени, а другие посчитают, что это поиск по уникальному ключу пользователя. Из этой ситуации можно легко выйти, создав специальный тип CustomerID (пусть даже он будет алиасом на ту же строку) и использовав его в прототипе функции GetUser(CustomerID), а вот поиск пользователя по имени можно назвать GetUserByName(string). Здесь уже нет никакой неопределённости.

Подобие — ещё одна распространённая причина ошибок. Если у вас есть переменные типа total1, total2, total3 — очень легко скопировать-вставить кусок кода и забыть исправить индекс. Код скомпилируется, а ошибка будет найдена (если будет) намного позже. Назвать эти переменные именами вроде sum, sum_of_squares, total_error — намного безопаснее.

Ещё одна беда — именование одной и той же сущности разными именами в разных модулях вашего кода. Это кажется не такой большой проблемой: «я назвал это так, в базу его другой программист положил под вот таким именем, а в UI его назвали вот так». Всё хорошо, пока всё хорошо. А потом что-то ломается и какой-то другой программист, чертыхаясь, пытается понять как связаны эти, совершенно не совпадающие по именам, вещи.

Проблемы двусмысленности и подобия присущи не только написанию исходного кода. В разных контекстах одни и те же слова могут означать разные вещи. Например, слово «заказчик» означает совершенно разные вещи в отделах закупок и продаж одной и той же компании. Таким образом, никогда не стоит бояться показаться смешным или неуместным, лишний раз переспросив всех вокруг, одинаково ли вы понимаете какие-то термины вашей предметной области.

Комментарии


Все мы видели примеры глупых комментариев новичков, типа:

// увеличиваем в цикле переменную i от 0 до 99
for(var i = 0; i < 100; i++) {

// присваиваем переменной а значение 4
var a = 4;

Да, выглядит немного туповато. Но даже у таких комментариев может быть смысл. Подумайте об изучении второго (или третьего) языка программирования. У вас уже есть знание синтаксиса одного языка, понимание всех этих условных переходов, циклов, функций — и вот вы изучаете то же самое в другом языке. Вам не нужно заново изучать данные понятия в новом языке, а лишь привязать у себя в голове вот такой формат цикла или присвоения к абстрактной идиоме «цикла» или «присвоения» — вот здесь могут и подобные комментарии пригодиться.

Как только эта привязка произошла — эти комментарии станут ненужным мусором, поскольку объяснение происходящего будет возникать у вас в голове уже при взгляде на сам код. По ходу того, как программист набирается опыта, его комментарии несут всё меньше информации о том, ЧТО делает код и всё больше о том ПОЧЕМУ и В КАКОМ КОНТЕКСТЕ он это делает. «Подход Х был выбран потому, что альтернативные подходы Y и Z не подошли по таким-то причинам», «при модификации данного кода следует помнить о том, что ...».

Хорошие комментарии дополяют ментальную модель понимания кода.

Контексты


Ограниченность рабочего лимита памяти приводит нас к необходимости декомозиции кода. Мы разбиваем сложный (или длинный) код на части, оперирующие ограниченным числом объектов. Но разбивать и декомпозировать тоже можно по-разному. Представьте себе, например, класс, лежащий очень глубоко в дереве наследования. И вот вы пишете в нём какой-то метод, который вызывает несколько других методов — один из того же класса, другой из «родителя», третий из «дедушки». Вроде бы ваш класс совсем прост — пара методов, строк по 5 в каждом. Но читать его код трудно, ведь даже чтение этих 10 строк требует создать в голове (и держать всё время чтения!) всё дерево наследования. Это трудно. Каждый новый слой наследования — это ещё одна идиома, занимающая и истощающая наш рабочий лимит памяти.

То же самое и с отслеживаем вызовов функций. Каждый шаг вглубь стека вызовов — шаг к лимиту наших ментальных возможностей.

Один из способов уменьшить глубину нашей ментальной модели контекстов — чётко разделить их. Одним из примеров может служать концепция раннего возврата («early return»):

public void SomeFunction(int age)
{
    if (age >= 0) {
        // сделать что-то
    } else {
        System.out.println("Не верный возраст");
    }
}

public void SomeFunction(int age)
{
    if (age < 0){
        System.out.println("Не верный возраст");
        return;
    }
    
    // сделать что-то
}

В первой версии при чтении кода мы доходим до части «сделать что-то» и ещё помним, что эта часть выполняется только при условии, указанном выше. Однако, когда мы доходим до части «else» мы уже достаточно далеко мысленно удалились от изначального условия и для понимания к чему-же относится это «else» нам нужно, во-первых, выбросить из головы только что прочитанную часть «сделать что-то», во-вторых вернуться назад к условию и осознать его и, в третьих, снова перейти к блоку «else» уже будучи в контексте условий, при которых мы в него попадём. Достаточно длинный путь.

Вторая версия кода применяет концепцию раннего возврата и поэтому является лучшей альтернативой. Мы сначала проверяем граничные условия и адекватно на них реагируем. Затем мы переходим к основному блоку кода и выполняем его. Никаких мысленных прыжков туда-обратно, никакой лишней смены контекстов.

Эмпирические правила


Одно из фундаментальных правил программирования гласит «Избегайте использования глобальных переменных». Но как на счёт случая, когда значение такой переменной присваивается лишь раз при инициализации и никогда не меняется в дальнейшем — это тоже проблема? Да, проблема. Дело здесь даже не в «переменности» или «глобальности». Мы вводим сущность, которая доступна отовсюду, а значит она явно или неявно будет присутствовать в любой ментальной модели кода, которую вы будете строить у себя в голове. Даже если это константа, даже если в данной функции она не используется — само по себе знание того, что есть нечто, что по своей воле (а не по воле данной функции) является видимым и доступным — уже даёт ему право претендовать на место в голове читателя данного кода. Конечно, мы не пишем «программы в вакууме», все они работают в каком-то окружении, и даже некоторые «допустимые» идиомы вроде Singleton обладают теми же свойствами. Так почему же они считаются лучшим вариантом, чем глобальные переменные?

Всё дело здесь в принципе единственной ответственности. Его предназначение — гарантировать, что каждая сущность в вашем коде служит одной цели. Не нулю, не двум — ровно одной. Это ограничение часто приводит к разбиению на более мелкие части. Это не всегда хорошо — подобным дроблением можно дойти до столь мелких частей, что собрать из них что-то реально полезное будет требовать серьёзных усилий. Кроме того, эти мелкие части могут быть настолько завязаны друг на друга, что от этого потеряется весь смысл принципа единственной ответственности.

Хорошим примером на эту тему может быть комментарий Кармака. Он показал вот эти три куска кода:

// A
void MinorFunction1( void ) {
}
 
void MinorFunction2( void ) {
}
 
void MinorFunction3( void ) {
}
 
void MajorFunction( void ) {
    MinorFunction1();
    MinorFunction2();
    MinorFunction3();
}
 
// B
void MajorFunction( void ) {
    MinorFunction1();
    MinorFunction2();
    MinorFunction3();
}
 
void MinorFunction1( void ) {
}
 
void MinorFunction2( void ) {
}
 
void MinorFunction3( void ) {
}
 
 
// C.
void MajorFunction( void ) {
    { // MinorFunction1
    }
 
    { // MinorFunction2
    }
    
    { // MinorFunction3
    }
}

Делая части кода меньшего размера, мы можем лучше добиться соответствия их содержимого их идее и названию. Однако, понимание кода от этого не становится лучше. Мы не можем читать код в линейном порядке (сверху вниз) и понимать его — вместо этого нам приходится перескакивать к коду вызываемых функций и обратно. Вериант С предлагает решение данной проблемы — выделение частей кода в логические области видимости, что, с одной стороны, сохраняет их целостность и разделение, а с другой — позволяет читать код последовательно и не терять контекст его выполнения.

На практике не существует идеального способа организации кода, поскольку кроме читабельности, о которой мы говорили выше, есть не менее важные вещи, такие как надёжность, поддерживаемость, производительность, скорость создания минимально рабочего прототипа и т.д. Некоторые из этих ценностей дополняют друг друга, но некоторые — прямо противоречат друг другу. В каждом отдельном случае важно понимать, что является ценностью данного конкретного проекта и на чём стоит сфокусировать усилия здесь и сейчас.
Инфопульс Украина 152,45
Creating Value, Delivering Excellence
Поделиться публикацией
Похожие публикации
Комментарии 64
    +1
    Здорово когда простым языком объяснят то, что воспринимаешь интуитивно. Теперь можно не просто сказать «так надо», а «так надо, потому что твой мозговой процессор так работает»
      +4
      И это будет неправдой.
      +1
      Очень годно, думаю к аналогичным выводам приходят многие программисты спустя N лет опыта.

      Вставлю свои пять копеек:
      Преобразование GetUser(string) в GetUser(CustomerID) + GetUserByName(string) тоже зависит от контекста. Пока ваша условная библиотека невелика по размерам, GetUserByName по сравнению с GetUser будет казаться тем самым шумом, который вы описали раньше. По мере же роста кодовой базы будет возрастать вероятность неоднозначного восприятия, и, следовательно, потребность в более «явной типизации».

      Хотя, на мой взгляд, энтерпрайзное тяготение к излишней типизации и длиннющим именам методов является попыткой решить проблемы некачественной ментальной модели средствами языка. Следует стремиться строить такие модели, в которых
      users.find(first_name, last_name) несет тот же смысл что и UserDAOSingleton().FindUserByFirstNameAndLastName(String FirstName, String LastName). Неоднозначность должна устраняться самой моделью, а не более длинным специализированным именем метода. В GetUserByName(string) до сих пор присутствует много неоднозначности, например возникает ли исключение если юзер не найден? А если найдено более одного юзера? Возвращает ли метод одного юзера, или их список, или итерируемый курсор? Ответы на такие вопросы должны быть в самой модели, а не в имени метода/комментариях.

      К слову, построение «правильной» (легкой, непротиворечивой, расширяемой, etc), ментальной модели это и есть основная сложность в программировании. Имея ментальную модель, написание кода становится просто рутиной. А код, написанный без четкой ментальной модели неизбежно превращается в набор костылей и говнокода. Грубо говоря, код это функция от ментальной модели и набора правил (языка программирования, стандартов, ограничений). При развитии почти любого проекта обычно меняется как ментальная модель, так и некоторые стандарты/ограничения. И когда этих изменений накапливается достаточно много, у программиста возникает желание «все переписать с нуля».

      Как следствие, при передаче кода, написании документации, или onboarding, в первую очередь нужно описывать именно ментальную модель, а не структуры данных и всякие API.
        0
        Так код и есть воплощение ментальной модели!
        Но как «красота в глазах смотрящих», так и понимание ментальной модели зависит от читающего.
        А вот что он увидит…
        И никак не проконтролируешь.
        Процесс передачи понимания асинхронный.
        Поэтому мне нравиться концепция ТДД.
        Как минимум машине проще объяснить чего ты хочешь, чем человеку. :-)
        –4
        Одним из примеров может служать концепция раннего возврата («early return»):

        public void SomeFunction(int age)
        {
            if (age < 0){
                System.out.println("Не верный возраст");
                return;
            }
            // сделать что-то
        }

        В контексте статьи это плохой пример, так как если после «сделать что-то» понадобится добавить код, обязательно выполняющийся в конце функции, то нужно будет учитывать, что где-то ранее может быть сделан преждевременный выход из функции — нужно учитывать больше контекста. С точки зрения читаемости такой код лучше:
        public void SomeFunction(int age)
        {
            if (age < 0){
                System.out.println("Неправильный возраст");
            } else {
                // сделать что-то
            }
        }

        Структурность и форматирование кода делает поток вычислений более очевидным в противовес выравненным неструктурным переходам.
          +4
          Пример с SomeFunction слишком абстрактен, но думаю автор подразумевает что код самодостаточен и завершен: функция решает только одну задачу, и задачи «добавить код выполняющийся в конце функции» попросту возникнуть не может. Что в данном примере вполне логично, т.к. это что-то вроде исключения на невалидных данных, и сложно придумать кейс где один и тот же код должен выполняться как в обычной, так и в исключительной ситуации.

          Может появиться задача делать что-то на каждом вызове SomeFunction, напрямую не относящееся к решаемой ею задаче (допустим считать время выполнения функции). Но раз это не имеет отношения к самой задаче, то и код должен находиться вне функции (например обернуть функцию декоратором, либо использовать какую-нибудь cross-cutting библиотеку).

          Либо же возникнет задача сделать что-то после вызова SomeFunction — но и делаться в таком случае это будет делаться отдельной функцией. И если последовательность вызовов этих двух функций несет смысловую нагрузку, тогда их можно объединить в третью функцию:
          public void SomeComplexAction(age){
              SomeFunction(age)
              SomeOtherFunction()
          }
            –1
            Пример с SomeFunction слишком абстрактен
            Да, абстрактен. Я это называю проблемой восприятия маленьких примеров, когда выводы делаются на основе заведомо суженных рамок, за которыми не видно проблем промышленного кода, которые можно наблюдать, например, регулярно анализируя сообщения об уязвимостях.
            думаю автор подразумевает что код самодостаточен и завершен: функция решает только одну задачу, и задачи «добавить код выполняющийся в конце функции» попросту возникнуть не может.
            А это ещё одна нагрузка мозг — нужно воспринимать не только текущий, но и предсказывать будущий контекст, что ещё хуже.
              +2
              Попробуйте привести контрпример, где нужно именно «добавить код выполняющийся в конце функции», особенно с учетом что early return здесь используется в качестве исключения. Я попытался привести два примера, где у, очевидного на первый взгляд, подхода «добавить код в конце функции» есть более удачные альтернативы.

              К слову, сама ваша формулировка «понадобится добавить код, обязательно выполняющийся в конце функции» для меня звучит порочно.

              Если следовать правилу, которое я привел раньше:
              source_code = F(mental_model, rules_and_restrictions)
              то есть всего три причины для изменения кода:

              1. Изменилась F (другими словами код не соответствует ментальной модели, т.е. в коде найден баг)
              2. Изменилась ментальная модель (например добавляем новую фичу)
              3. Изменились правила/ограничения (меняем ЯП, разбиваем монолит на (микро)сервисы, меняем фреймворк, применяем style guidelines, etc)

              Задача «добавить код» сама по себе в принципе не возникает.
                –2
                Не поверю, что Вы не можете придумать примеры сами. Я же не буду придумывать, а покажу функцию от Apple, где есть код, который должен выполнится в конце habr.com/post/213525. Кстати, эта конкретная ошибка могла произойти и с return.

                Добавить код может понадобиться, когда, к примеру, расширяются коды проверки или изначально что-то забыто, или происходит рефакторинг или ещё по тысяче причин.

                Задача «добавить код» сама по себе в принципе не возникает.
                Как и задача писать код сама по себе в принципе не возникает. Но мы же и пишем, и добавляем?
                  +3
                  Вот я и постарался придумать в том контексте: early return в качестве исключения. Придумать ситуацию когда один и тот же код должен выполняться в случае исключения и в случае его отсутствия мне сложно.

                  Но мы видимо друг друга не понимаем. В приведенном вами коде Apple задача «очистить память перед выходом» стояла изначально. Вы же, как мне показалось, утверждали что подобная потребность может появиться внезапно.

                  Так или иначе, вот на мой взгляд более наглядный пример, демонстрирующий преимущество early return:
                  def func1():
                      results = None
                      user = get_user()
                      if user:
                          orders = get_orders(user)
                          if orders:
                              transactions = get_transactions(orders):
                              if transactions:
                                  results = [execute(t) for t in transactions]
                              else:
                                  print('no transactions')
                          else:
                              print('no orders')
                      else:
                          print('no user')
                      return results
                  
                  def func2():
                      user = get_user()
                      if not user:
                          print('no user')
                          return None
                  
                      orders = get_orders(user)
                      if not orders:
                          print('no orders')
                          return None
                  
                      transactions = get_transactions(orders)
                      if not transactions:
                          print('no transactions')
                          return None
                      return [execute(t) for t in transactions]
                  


                  Я думаю большинство согласится, что ментальная нагрузка в func2 гораздо ниже. Ментальный стек не переполняется, взгляду не нужно прыгать по if/else блокам, читать и поддерживать такой код гораздо проще.

                  Естественно, если изначально стоит задача делать нечто перед выходом из функции, и язык не поддерживает try-finally либо RAII/contextmanager, то early return будет неуместен.
                    –1
                    В приведенном вами коде Apple задача «очистить память перед выходом» стояла изначально
                    А если я скажу, что изначально шла работа с локальными буферами и освобождать не нужно было, но в процессе обновления библиотеки понадобилось, то это что-то изменит? Если не стоит запрет на преждевременный выход и я привык его применять, это не увеличит нагрузку на распознавание ситуации, что дальше есть необходимые действия и так поступать нельзя?
                    Читать и поддерживать такой код гораздо проще.
                    Вот я из большинства поверил Вам, взял этот код и в процессе его более простого сопровождения он стал таким.
                    def func2():
                        user = get_user()
                        if not user:
                            print('no user')
                    
                        orders = get_orders(user)
                        if not orders:
                            return 'no orders', None
                    
                        transactions = get_transactions(orders)
                        if not transactions:
                            return 'no transactions', None
                    
                        return 'OK', [execute(t) for t in transactions]

                    Уменьшилась ли от этого ментальная нагрузка на работу с этой функцией по сравнению со структурным потоком исполнения?
                    И, кстати, почему 1-й вариант Вы записали не так?
                    def func1():
                        results = None
                        user = get_user()
                        if not user:
                            print('no user')
                        else    
                            orders = get_orders(user)
                            if not orders:
                                print('no orders')
                            else
                                transactions = get_transactions(orders):
                                if not transactions:
                                    print('no transactions')
                                else:
                                    results = [execute(t) for t in transactions]
                        return results
                    

                    Ведь он же ближе ко 2-му? И что значит прыгать по if/else? Вы действительно прыгаете, затрачивая энергию, а не читаете наглядное ветвление? Может, с непривычки?
                      +2
                      Если не стоит запрет на преждевременный выход и я привык его применять, это не увеличит нагрузку на распознавание ситуации, что дальше есть необходимые действия и так поступать нельзя?
                      Безусловно увеличит. Но, как я уже сказал, проблема возникает лишь в тех средах, где нет возможности использовать try-finally или его аналоги, т.е. ничтожно редко.

                      Уменьшилась ли от этого ментальная нагрузка на работу с этой функцией по сравнению со структурным потоком исполнения?
                      Не совсем понял вопрос. В любом случае, этот вариант меня так же устраивает (за исключением опечаток), т.к. основная работа метода все так же выполняется на первом уровне вложенности.

                      И, кстати, почему 1-й вариант Вы записали не так?
                      Можно и так. Читается лучше, но все равно плохо — каждый вложенный if грузит мой ментальный стек. Если у вас десяток таких проверок будет — вы десять вложенных if-блоков будете делать?

                      Плюс, в таком стиле пишут крайне редко, т.к. он противоречит естественному (позитивному) ходу мыслей. В if обычно пишут позитивное условие, в else — негативное, а не наоборот. Начинать if с отрицания, при наличии else, часто считается code smell.

                      т.е. вместо
                      if not condition:
                          # something bad
                      else:
                          # something good

                      практически всегда будет
                      if condition:
                          # something good
                      else:
                          # something bad
                        –1
                        Не совсем понял вопрос. В любом случае, этот вариант меня так же устраивает (за исключением опечаток)
                        Дело именно в «опечатках». Статья, напомню, о психологии читабельности, и Ваш комментарий о более лёгком сопровождении.

                        Плюс, в таком стиле пишут крайне редко, т.к. он противоречит естественному (позитивному) ходу мыслей. В if обычно пишут позитивное условие, в else — негативное, а не наоборот. Начинать if с отрицания, при наличии else, часто считается code smell.

                        А это, тогда что такое?
                        if not user:
                                print('no user')
                                return None

                        От того, что в таком случае else неявный, делает только хуже в контексте статьи.
                          0
                          проблема возникает лишь в тех средах, где нет возможности использовать try-finally или его аналоги
                          Отнюдь. Освобождение ресурсов это лишь один из примеров, где важна последовательность действий. Растаскивание же порядка действий по defer, try finally и им подобным совсем не улучшает читаемость. А ведь статья об этом. Уменьшения разнообразия переходов положительно сказывается на понятности. Это давно заметили ещё на goto, но этим оператором дело ограничивается.
                          0
                          func2() у вас может упасть, если get_orders(user) не воспринимает нормально get_orders(false) или что там проверяется в not user.

                          В коде func1() надо тщательно читать каждое условие, чтобы понимать где success path, а где ошибки, плюс следить за значением results. Может это можно отреглировать на уровне соглашений команды/проекта, что success path идёт исключительно в else и состояния меняются исключительно в ней, но, субъективно, это сложнее чем принцип early return «exception». За ним, кстати, тоже нужно следить, чтобы не нарушали.
                            0
                            func2() у вас может упасть
                            В этом и суть. Статья о чём? О читабельности. Что читабельней: наличие всех return очевидный поток исполнения?
                            В коде func1() надо тщательно читать каждое условие, чтобы понимать где success path
                            Не более внимательно, чем в раннем выходе.
                            плюс следить за значением results
                            Питон это отдельная песня, но при использовании более ошибкоустойчивых языков(+анализатор), корректность работы с result отслеживается. Попробуйте отследить пропущенный return.
                              0
                              Попробуйте отследить пропущенный return.
                              Весьма странный аргумент.

                              Но в любом случае, вот вам аналогичная опечатка в вашем коде, попробуйте ее отследить. Не думаю что отсутствие early return вам хоть как-то поможет.

                              def func1():
                                  results = None
                                  user = get_user()
                                  if not user:
                                      print('no user')
                                  else    
                                      orders = get_orders(user)
                                      if not user:
                                          print('no orders')
                                      else
                                          transactions = get_transactions(orders):
                                          if not transactions:
                                              print('no transactions')
                                          else:
                                              results = [execute(t) for t in transactions]
                                  return results
                              
                                0
                                Весьма странный аргумент.
                                Напоминаю, что речь в статье о читабельности.
                                Но в любом случае, вот вам аналогичная опечатка в вашем коде
                                Во-первых, код не мой, а переделанный Ваш, так как мне было непонятно, почему Вы не записали 1-й вариант так. Во-вторых, ошибка не аналогичная, но тоже отслеживается, хотя и не стандартными средствами Python. Как я уже писал, Python — это отдельная песня.
                                Чтобы ошибка была, действительно, аналогичной, она должна была бы выглядеть так:
                                def func1():
                                    results = None
                                    user = get_user()
                                    if not user:
                                        print('no user')
                                
                                    orders = get_orders(user)
                                    if not orders:
                                        print('no orders')
                                    else:
                                        transactions = get_transactions(orders):
                                        if not transactions:
                                            print('no transactions')
                                        else:
                                            results = [execute(t) for t in transactions]
                                    return results
                                Как и прежде не видите разницы в читаемости структурного потока и выравненного неструктурного?
                                  +1
                                  Напоминаю, что речь в статье о читабельности.
                                  Вот именно. А не о случайных опечатках, они из другой оперы.

                                  Во-вторых, ошибка не аналогичная, но тоже отслеживается
                                  Как я понял, ваш аргумент состоит в том, что если в функции несколько return, то сложно отследить когда один из них из-за опечатки удалят и это приведет к runtime error. На мой взгляд этот аргумент очень слаб — ведь аналогичных ошибок можно придумать бесконечное множество, и защищать себя от одной ошибки из бесконечности довольно бесполезно. Поэтому и привел аналогичный, на мой взгляд пример: опечатка, не бросающаяся в глаз, приводящая к runtime error.

                                  Если я неправильно понял класс опечаток, от которых вы пытаетесь защититься — тогда поясните о каком именно классе идет речь.

                                  И вы до сих пор не ответили: вместо 10 return вы тоже предпочтете сделать 10 уровней вложенных if/else?
                                    0
                                    Вот именно. А не о случайных опечатках
                                    Отчего же, если в одном случае читаемость потока исполнения выше настолько, что случайная опечатка не сможет пройти незамеченной?
                                    Как я понял, ваш аргумент состоит в том, что если в функции несколько return, то сложно отследить когда один из них из-за опечатки удалят и это приведет к runtime error
                                    Нет, речь об очевидности структурного потока. Пример от Apple, приведший к серьёзной уязвимости, был о том же, за тем исключением, что там использовался goto, но это не так принципиально. Ошибка оставалась незамеченной, несмотря на то, что Вы и другие мои критики, считают что неструктурный преждевременный выход лучше читается. В то же время со строго структурным потоком такую ошибку не только было бы сложно допустить, но и была бы она намного очевидней.
                                    На мой взгляд этот аргумент очень слаб — ведь аналогичных ошибок можно придумать бесконечное множество, и защищать себя от одной ошибки из бесконечности довольно бесполезно
                                    Мы рассмотрели отдельный аспект, и было бы странно, если бы нам удалось охватить все случаи. Это не защита от одной ошибки, где Вы это увидели? От защиты от ошибок памяти и арифметического переполнения Вы тоже будете открещиваться на том основании, что ошибок можно придумать бесконечное множество?

                                    И вы до сих пор не ответили: вместо 10 return вы тоже предпочтете сделать 10 уровней вложенных if/else?
                                    Похоже, я просто не обратил внимание на этот вопрос. Это зависит от задачи. Если в коде встречается виртуальная лесенка из 10 return (а не switch), то скорее всего, там проблема на архитектурном уровне, и надо не потакать ей неструктурными подходами, а решать. Впрочем, всегда надо смотреть на конкретику, для универсального ответа недостаточно данных.
                                      0
                                      Отчего же, если в одном случае читаемость потока исполнения выше настолько, что случайная опечатка не сможет пройти незамеченной?
                                      Стоп, а почему вы решили что не может? На мой взгляд все ровно наоборот: в вашем примере заметить опечатку гораздо сложнее. Пропущенный return резко выбивается из общего стиля, а вот отсутствие else выглядит вполне органично.

                                      Если в коде встречается виртуальная лесенка из 10 return (а не switch), то скорее всего, там проблема на архитектурном уровне
                                      То же самое можно сказать о двух goto в коде Apple, да и в принципе о любой проблеме. У вас получается нефальсифицируемая теория: любой недостаток вы списываете на внешние причины. Моя теория работает везде, а там где она не работает — это у вас плохая архитектура. Неконструктивно.
                                        0
                                        Стоп, а почему вы решили что не может? На мой взгляд все ровно наоборот: в вашем примере заметить опечатку гораздо сложнее
                                        А Вы попробуйте 1. совершить эту ошибку. 2. не заметить сместившийся блок. 3. Сопоставьте это с изменением потока управления из-за наличия или отсутствия линейного оператора.
                                        То же самое можно сказать о двух goto в коде Apple
                                        О том и речь, и это напрямую связано с наличием неструктурных переходов.
                                        У вас получается нефальсифицируемая теория
                                        Какая теория? О том, что в заданном вопросе недостаточно информации, для универсального ответа? Или Вас удивляет, что наличие «потребности» в большом количестве неструктурных переходов свидетельствует о проблемах проектирования? Об этом пишут ещё с 70-х. Вы знакомы с классикой Дейкстры? Знакомы с рекомендациями MISRA C для критического к корректности ПО и им подобным? Моя «нефальсифицируемая теория» написана практикой задолго до меня. Но сообщество, к сожалению, ходит по кругу.
                                          0
                                          А Вы попробуйте
                                          Попробовал, получилось довольно легко, т.к. код выглядит валидно. В обоих вариантах это выглядит не как exception, а как warning, вот и все.

                                          Или Вас удивляет, что наличие «потребности» в большом количестве неструктурных переходов свидетельствует о проблемах проектирования? Об этом пишут ещё с 70-х. Вы знакомы с классикой Дейкстры?
                                          Представьте себе, знаком. Вот только сейчас не 70-е. При фанатичном следовании «классике» Дейкстры придется отказаться не только от multiple return, но и от break/continue в циклах, и от try/catch/finally в языках с исключениями. К счастью, фанатиков нынче не много, да и сам Дейкстра сегодня вряд ли бы поддержал фанатиков.

                                          Знакомы с рекомендациями MISRA C для критического к корректности ПО и им подобным?
                                          А вас не смущает что 'эти рекомендации официально допускают отклонения? И что они написаны для конкретной индустрии (embedded), конкретно для С (где памятью приходится управлять вручную), и вовсе не претендуют применение в других сферах? А может даже вообще ни на что не претендуют.

                                          Обсуждению на самом деле сто лет в обед, люди давно пришли к консенсусу:
                                          stackoverflow.com/questions/36707/should-a-function-have-only-one-return-statement
                                            0
                                            Представьте себе, знаком. Вот только сейчас не 70-е
                                            Зачем мне представлять? Если мой риторический вопрос заключался в указании на то, что это не у меня получается нефальсифицируемая теория, а написана она уже давно на основе практики. То, что о ней пишут с 70-х это не означает, что эти аргументы устарели.
                                            При фанатичном следовании «классике» Дейкстры придется отказаться не только от multiple return, но и от break/continue в циклах, и от try/catch/finally в языках с исключениями. К счастью, фанатиков нынче не много, да и сам Дейкстра сегодня вряд ли бы поддержал фанатиков.
                                            Если Вы пишите об этом как о фанатизме, и даже делаете предположения о поддержке со стороны Дейкстры, то это значит, что Вы, как минимум, невнимательно отнеслись к его трудам.
                                            А вас не смущает что 'эти рекомендации официально допускают отклонения? И что они написаны для конкретной индустрии (embedded), конкретно для С (где памятью приходится управлять вручную)
                                            Вы незнакомы с MISRA C, иначе бы не стали ничего писать про выделение памяти вручную. А также знали бы, что лишь часть этих рекомендаций специфично для встроенных применений. Единственный выход не относится к ним, а вот неиспользование динамически распределяемой памяти — относится.

                                            Возвращаясь к теме статьи, я вполне допускаю, что для Вас неструктурный поток исполнения является более читаемым. Но моё мнение основано на анализе ошибок, в том числе и приведшим к уязвимостям, я не делаю упор на «я так вижу».

                                            Ну и напоследок, как Вы думаете, всё-таки есть люди, для которых структурный поток более читаемый, или это непременно фанатики из 70-х?
                                              0
                                              Если в коде встречается виртуальная лесенка из 10 return (а не switch), то скорее всего, там проблема на архитектурном уровне, и надо не потакать ей неструктурными подходами, а решать.

                                              «лесенка» растет из-за вложенных if, циклов и switch. Например, здесь «хороший» пример кода с 14-м уровнем вложенности. Я адаптировал этот код через early return, получилось втрое меньше строк и четыре уровня вложенности. Как думаете, стало проще?

                                              Если Вы пишите об этом как о фанатизме, и даже делаете предположения о поддержке со стороны Дейкстры, то это значит, что Вы, как минимум, невнимательно отнеслись к его трудам.

                                              Давайте на примере. Дейкстра предлагает отказаться от goto. В си нет автоматического управления ресурсами, поэтому перед возвратом их надо освобождать. Очищать ресурсы перед каждым early return неудобно, в си можно делать через goto, например
                                              так
                                              void foo(...) {
                                                  resource1 = ...
                                                  if (плохо1)
                                                      goto cleanup1;
                                                  //...
                                                  resource2 = ...
                                                  if (плохо2)
                                                      goto cleanup2;
                                                  //...
                                              cleanup2:
                                                  (очистка ресурса 2);
                                              cleanup1:
                                                  (очистка ресурса 1);
                                              }


                                              Это достаточно удобно, но здесь присутствует goto. Следовательно, следуя заветам Дейкстры, следует отказаться от раннего возврата. Однако, с тех пор: 1. появилось много языков с автоматическим освобождением ресурсов. 2. люди отвыкли использовать goto там, где его можно заменить циклами, он начал встречаться крайне редко и для упрощения кода (мой пример).

                                              Но моё мнение основано на анализе ошибок, в том числе и приведшим к уязвимостям, я не делаю упор на «я так вижу».

                                              вы делаете упор именно на это.

                                              Ну и напоследок, как Вы думаете, всё-таки есть люди, для которых структурный поток более читаемый, или это непременно фанатики из 70-х?

                                              что важнее — факт существования или относительное количество?
                                                0
                                                «лесенка» растет из-за вложенных if, циклов и switch. Например, здесь «хороший» пример кода с 14-м уровнем вложенности.
                                                Отличный пример говнокода и беглый просмотр показал, что с ошибками. Ранний выход в нём используется, поэтому точно можно сказать, что его проблемы не в том, что кто-то запретил использовать ранний выход. Его, конечно, можно улучшить, даже если не ставить цель сделать напрашивающуюся декомпозицию и соблюдать структурность. Только это не имеет отношения к вопросу. Впрочем, можете показать Ваш код. А ещё лучше, отправьте его в Microsoft.

                                                Следовательно, следуя заветам Дейкстры, следует отказаться от раннего возврата. Однако, с тех пор: 1. появилось много языков с автоматическим освобождением ресурсов.
                                                Во-первых, Вы путаете заветы и доводы. Во-вторых, освобождение ресурсов — это лишь часть ситуаций, где важно соблюдать порядок. Ранее я давал ссылку на эпичный промах Apple, где не было проблемы с освобождением ресурсов, но была проблема с неструктурными переходами. Вы не заметили этого или не придали значения, наверно, отчасти поэтому написали это:
                                                я не делаю упор на «я так вижу».
                                                вы делаете упор именно на это.
                                                Интерпретация чужих слов — это, во многом, вопрос выбора. Ваш выбор таков, это нормально.
                                                что важнее — факт существования или относительное количество?
                                                Важно всё. Даже то, каким образом получены данные о количестве. Если методом опроса, то у меня есть плохие новости, если по серии экспериментов и наблюдений, то совсем другое дело.
                                                  0
                                                  исследование по поводу эффективности MISRA C:
                                                  спойлер
                                                  A study at the TU Delft, by Cathal Boogerd and Leon Moonen, empirically assesses the value of MISRA C:2004. It comes to similar results:[22]
                                                  From the data obtained, we can make the following key observations. First, there are 9 out of 72 rules for which violations were observed that perform significantly better (α = 0.05) than a random predictor at locating fault-related lines. The true positive rates for these rules range from 24-100%. Second, we observed a negative correlation between MISRA rule violations and observed faults. In addition, 29 out of 72 rules had a zero true positive rate. Taken together with Adams' observation that all modifications have a non-zero probability of introducing a fault, this makes it possible that adherence to the MISRA standard as a whole would have made the software less reliable.


                                                  Итог: софт, следующий заветам MISRA C, менее надежен.
                                                  И вообще, вы называете «неструктурным» код, в котором структура прослеживается проще.
                                                    0
                                                    TimTowdy уже давал ссылку на материал, из которого взята эта цитата. И я уже приводил 2-ю цитату из этой же статьи, находящейся в следующем абзаце.
                                                    Selection of rules that are most likely to contribute to an increase in reliability maximizes the benefit of adherence while decreasing the necessary effort. Moreover, empirical evidence can give substance to the arguments of advocates of coding standards, making adoption of a standard in an organization easier.
                                                    Это Википедия, пора бы привыкнуть. Отчасти, из-за такой аккуратности в цитировании, несмотря на то, что сами авторы исследования считают, что их эмпиричиеские данные дают аргументы сторонникам MISRA С, ссылающихся на них люди умудряются прийти к прямо противоположным выводам, не читая самого исследования и не понимая к чему относятся слова из цитаты в Википедии.
                                                    И вообще, вы называете «неструктурным» код, в котором структура прослеживается проще.
                                                    Я пользуюсь общепринятой терминологией, которую, возможно, Вам тоже следует изучить прежде, чем что-то брать в кавычки.
                                                      0
                                                      Я пользуюсь общепринятой терминологией, которую, возможно, Вам тоже следует изучить прежде, чем что-то брать в кавычки.

                                                      превосходный совет, рекомендую. Принцип “Single entry, single exit” (на «нарушение» которого вы ссылаетесь называя код неструктурным) значит, что у функции одна точка входа и одна точка выхода. То есть, она возвращается в одно место в коде, а не из одного места. Т.к. все return ведут в одну точку кода программы, этот принцип выполняется независимо от числа return внутри функции. Опять же, механизм исключений, появившийся позже, противоречит принципу sese. Значит ли, что стоит убирать исключения из всех яп их использующих? Нет.
                                                        0
                                                        превосходный совет, рекомендую
                                                        Но сами Вы им не воспользовались, потому что то, что Вы описали не имеет отношение к структурному потоку выполнения. Всё-таки, неплохо бы почитать про структурное программирование, хотя бы и на Википедии.
                                                        Опять же, механизм исключений, появившийся позже, противоречит принципу sese. Значит ли, что стоит убирать исключения из всех яп их использующих? Нет.
                                                        Из уже используемых языков ничего удалять нельзя, можно только добавлять — такова логика развития языка.

                                                        В новых же языках исключения начали убирать, даже не ставя задачу сделать поток выполнения структурным. В Go их задвинули в дальний угол в виде panic, предпочитая возвращать ошибку в коде возврата. В Rust исключений нет, они обходятся специальными типами на основе шаблонов и макросами. В Swift обработку ошибок попытались внешне максимально приблизить к старому подходу с исключениями, но семантически это ближе к подходу Rust. Эта тенденция не имеет отношения к борьбу за структурность, но как видим, к исключениям есть претензии помимо этого.
                                                          0
                                                          Понятия «структурный поток выполнения» не существует так же, как и не существует понятия «неструктурный код» в контексте кода, использующего early return. Хватит придумывать новый термин каждый раз когда заканчивается аргументация.

                                                          Новые языки go/rust по дизайну реализуют обработку ошибок через early return, и эмулируют исключения через возврат вариантов код ошибки/значение. Для доказательства актуальности гайдлайнов, появившихся до си, пример крайне неудачный
                                                            0
                                                            Понятия «структурный поток выполнения» не существует так же, как и не существует понятия «неструктурный код» в контексте кода, использующего early return. Хватит придумывать новый термин каждый раз когда заканчивается аргументация.
                                                            Структурный поток выполнения — это не новый термин, придуманный мной, а всего лишь сочетание слов, которые я использовал как синоним для кода, написанного в соответствии с принципами структурного программирования. Я вижу, это Вас смущает, но таковы особенности обычного человеческого языка — они позволяют достаточно вольно обращаться со словами. Я так понимаю, Вы сконцентрировались на принципе «один вход — один выход», поэтому упустили из виду, что он не исчерпывающий, отсюда и недопонимание.
                                                            Для доказательства актуальности гайдлайнов, появившихся до си, пример крайне неудачный
                                                            Не знаю, почему Вам показалось, что эти языки были приведены для доказательства рекомендаций, появившихся до C(особенно MISRA C, конечно), так как я сразу написал, что в них даже не ставили задачу сделать код структурным. В действительности, это было ответ на это:
                                                            Значит ли, что стоит убирать исключения из всех яп их использующих? Нет.
                                                            Вопрос был задан так, будто это совсем уже что-то безумное. А как видим, нет — исключение исключений стало банальным действом.
                                                              0
                                                              Не знаю, почему Вам показалось, что эти языки были приведены для доказательства рекомендаций, появившихся до C(особенно MISRA C, конечно), ...

                                                              в том абзаце мы говорили про «структурный код» (1968). Си появился в 1969. Теперь по части эффективности single return:
                                                              Table 2: The noisiest rules in decreasing value of D statistic in MISRA C 1998

                                                              10.1 Implicit conversion of integer types
                                                              13.1 No assignment in Boolean valued expressions
                                                              14.8/14.9 if, while and for must be compound statements
                                                              11.1-4 Conversions between pointers
                                                              12.7 No bitwise operations on signed types
                                                              12.5 Operands of logical operators must be primary expressions
                                                              16.8 All exits in non-void function must have a return
                                                              14.7 A function shall have a single point of exit

                                                              эти правила ухудшают среднее качество кода

                                                              Вы привели в качестве аргумента в споре “early return vs single return” новые языки, дизайн которых опирается на early return. Поздравляю, you played yourself.
                                                              Это они чушь какую-то написали, не посоветовавшись с Вами?

                                                              да.
                                                                0
                                                                Не знаю, почему Вам показалось, что эти языки были приведены для доказательства рекомендаций, появившихся до C
                                                                в том абзаце мы говорили про «структурный код» (1968). Си появился в 1969.
                                                                1. C появился в 1972, не путайте с B
                                                                2. Структурное программирование — это дисциплина, а не гайдлайн. Не удивительно что я Вас не понял.
                                                                3. «В том абзаце» мы не могли говорить, что бы Вы не имели ввиду, это реплика отдельного человека. А что Вы имеете ввиду довольно сложно понять, и, соответственно, сложно понять, в чём Ваши претензии, хотя картина всё ясней.

                                                                Теперь по части эффективности single return
                                                                Вы так поспешили скопировать этот кусок, что не смогли понять:
                                                                1. В MISRA C 1998 нет правила 14.7. Это относится к следующей надписи MISRA C 2004
                                                                2. Если Вы всё-таки посмотрите список для MISRA C 1998, заметите что-то странное
                                                                3. Шумные правила не означают, что они ухудшают качество кода, это всего лишь означает, что в конкретном исследовании положительная корреляцияоказалась невелика
                                                                Из чего можно сделать вывод, что Вы не пытались понять, о чём говорится в исследовании и что это означает.

                                                                Вы привели в качестве аргумента в споре “early return vs single return” новые языки, дизайн которых опирается на early return. Поздравляю, you played yourself.

                                                                Вы, конечно, можете эти мои недвусмысленные слова:
                                                                В новых же языках исключения начали убирать, даже не ставя задачу сделать поток выполнения структурным.
                                                                Трактовать так, но могу лишь посочувствовать.

                                                                Это они чушь какую-то написали, не посоветовавшись с Вами?
                                                                да.
                                                                Это, как раз, означает, что Вы придумываете свои собственные понятия, и Ваше понимание единственного выхода противоречит общепринятому.
                                                                  0
                                                                  the noisest rules in decreasing value of D statistic in MISRA C 1998

                                                                  «зашумленно ухудшающие код» правила из MISRA C “1998”. Но разумеется это я читаю и ничего не понимаю, ага.

                                                                  Общепринятая трактовка в данном случае не является оригинальной. И вы бы об этом знали, следуй вы собственным советам
                                                              0
                                                              Загляните ещё в английскую Википедию
                                                              The most common deviation, found in many languages, is the use of a return statement for early exit from a subroutine. This results in multiple exit points, instead of the single exit point required by structured programming.
                                                              Это они чушь какую-то написали, не посоветовавшись с Вами?
                                                  0
                                                  То, что о ней пишут с 70-х это не означает, что эти аргументы устарели.
                                                  В 70-з не было аргумента «если мои советы вам не подходят, то у вас говнокод». Это ваше новшество.

                                                  Но моё мнение основано на анализе ошибок, в том числе и приведшим к уязвимостям, я не делаю упор на «я так вижу».
                                                  Пустые слова, к сожалению. Никакой аналитики вы не привели. А по ссылке которую я привел сходите, там как раз аналитика по MISRA.

                                                  Ну и напоследок, как Вы думаете, всё-таки есть люди, для которых структурный поток более читаемый, или это непременно фанатики из 70-х?
                                                  Зависит то того, что вы здесь подразумеваете под структурным потоком. Если речь только о single return в функциях, то конечно есть, это во многом дело вкуса. Если же полный отказ от break/continue в циклах и try/catch/finally — то это, безусловно, фанатизм.
                                                    0
                                                    В 70-з не было аргумента «если мои советы вам не подходят, то у вас говнокод». Это ваше новшество.
                                                    А где Вы это увидели, не расскажете? Интересно же. Вы же не станете вводить других в заблуждение?
                                                    Пустые слова, к сожалению. Никакой аналитики вы не привели. А по ссылке которую я привел сходите, там как раз аналитика по MISRA.
                                                    А что Вас смутило даже в этой статье по исследованию 1-го проекта? Это:
                                                    Moreover, empirical evidence can give substance to the arguments of advocates of coding standards, making adoption of a standard in an organization easier. However, correlations and true positive rates as observed in this study may differ from one
                                                    project to the next
                                                    Или что-то другое в отрыве от контекста? Например то, что внесения изменений для соблюдения стандарта в уже готовый отлаженный код приводит к появлению дополнительных ошибок? Так стандарты нужны не для изменений в готовом коде, то есть дополнительных действиях, а для соблюдения во время написания нового без дополнительных усилий.
                                                    Если же полный отказ от break/continue в циклах и try/catch/finally — то это, безусловно, фанатизм.
                                                    Я просто хочу обратить внимание на то, что немногим ранее Вы обвинили меня в том, что я будто-бы утверждаю «если мои советы вам не подходят, то у вас говнокод». А теперь заявляете, что несогласные с Вами — это, безусловно, фанатики. Похоже на зеркалирование собственных поступков, нет?
                  • НЛО прилетело и опубликовало эту надпись здесь
                      –1
                      Несколько раз встречал ошибки что в начинают добавлять код после закрывающего блока для else, а на самом деле этот код должен быть использован внутри else.
                      К сожалению, не понял, что имеется ввиду.

                      А пример хорошо переписывается в многоветочный if, Java не имеет встроенной поддержки, но благодаря статическим анализаторам это не так критично
                      public void SomeFunction(int age)
                      {
                          if (age < 0) {
                              System.out.println("Не верный возраст");
                          } else if (age > 100) {
                              System.out.println("Подумай ещё");
                          } else if (age < 18) {
                              System.out.println("Не продаём малолетним");
                          } else {
                             // сделать что-то
                          }
                      }

                      С преждевременным выходом же появляется дополнительная возможность ошибки пропуска return и диагностировать это сложней в общем случае
                      • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          Про «else if» пример был для самых простых случаях. Обычно это что-то типа
                          Тогда нужно разбивать на шаги. Ибо иначе огромные функции так и растут. Из-за усеянности неструктурными переходами разбивать функцию становится всё сложней.

                          Как-то, читая новость об исправлении уязвимости в NGINX, случайно обнаружил в нём другую ошибку переполнения. Оцените исправление этой ошибки hg.nginx.org/nginx/rev/e3723f2a11b7 Она была продублирована в большом количестве мест, и общий код не мог быть так просто вынесен в отдельную оттестированную функцию, а был повсюду в большущих функциях, потому что использовалась логика неструктурных переходов, усложняющая декомпозицию.
                          • НЛО прилетело и опубликовало эту надпись здесь
                              0
                              Если он состоит из вложенных if-ов, то он поддаётся декомпозиции. Ранний выход провоцирует на дальнейшее разрастание.

                              Про неидеальный мир согласен, но в контекст статьи это не вписывается. Тут, всё-таки, о правильном подходе даже с учётом психологии.
                                0
                                Один раз сделал конструкцию, очень красиво получалось, пока не начал заполнять «пробелы» между элс-ифами. Думал по бырому сделаю копированием, чтоб проверить что будет на выходе… получилось овер 5к строк для печати одной формы… естественно, ужалось всё это в 500.
                                  0
                                  А можете рассказать чуть подробней? Если Вам удалось ужать 5000 строк до 500, то хотелось понять как.
                                    0
                                    Я там вообще всю логику переделал, методы переписал (я же говорю, изначально хотел по бырому копи-пастом заполнить и проверить как работает...). Проблема в том, что уже полгода никому это не нужно. Было сказано, что для пользователей сложно, пуская пишут вручную. И недельные потуги пришлось умножить на «0».
                          0
                          } else if (age > 100) {
                          System.out.println(«Подумай ещё»);

                          Вообще-то люди старше ста лет существуют.
                          Так что код так же неверно построен, как и база данных в которой у всех людей есть фамилия, имя и отчество — и каждое поле не длиннее 16 символов, никак не учитывающая то, что клиент может оказаться иностранцем.
                          • НЛО прилетело и опубликовало эту надпись здесь
                              0
                              Чего спорить то? 150 поставьте. Вопрос был в структуре, а не в примере.
                        –1
                        С точки зрения читаемости такой код лучше:

                        не согласен, категорически. Early return/throw — такой же шаблон восприятия, что и обычный for, его назначение интуитивно понятно, а позитивный и негативный сценарии выполнения легко отличимы при беглом осмотре. И уже бонусом к этому идет уменьшение уровней вложенности.
                        –1
                        public void SomeFunction(int age)
                        {
                            if (age >= 0) {
                                // сделать что-то
                            } else {
                                System.out.println("Не верный возраст");
                            }
                        }
                        
                        public void SomeFunction(int age)
                        {
                            if (age < 0){
                                System.out.println("Не верный возраст");
                                return;
                            }
                            
                            // сделать что-то
                        }


                        Лично мне первый вариант является более приемлемым. Так как при большем количестве параметров функции возникает комбинаторный взрыв количества возможных значений и сочетаний этих параметров. И на много проще оказывается очертить множество подходящих значений, а все остальное по умолчанию считать мусором и выбрасывать ошибку (ну или как-то по другому обрабатывать эту ситуацию). По опыту так меньше ошибок вылазит в итоге.
                          0
                          По поводу комментариев.
                          Чем проще функция или метод — тем проще комментарий, но бывает и переизбыток, А при усложнении — их недостаток. Да, нужно комментировать. Написан код, а про комменты все забыли. Вот и начинается
                          а + б // а+б
                          а+б*с/г // какая-то формула
                          20 строк кода // метод делает то, потому что это вот так вот
                          50 строк кода // метод делает то
                          100 строк кода // метод
                          500 строк кода // магия, потому что по другому никак.

                            0
                            Порой комментарии напоминают интимную переписку надписи, нацарапанные гвоздем на стенах камеры =)
                            +1
                            Возможно, концепцию раннего возврата стоит расширить до «при множественном выборе ставьте крупные блоки вниз, мелкие — наверх».
                            Прекрасная статья!
                              0
                              Вопросы красоты записи обычных структур — наверное. Хотя, условный питон по удобности циклов трудно превзойти. Куда важнее, чтобы язык программирования давал возможности для добавления семантики через синтаксис.

                              Каждый раз, когда у вас в коде появляется осмысленное имя внутри синтаксической конструкции, код становится понятнее.

                              Начинаем с имён переменных (вместо номеров ячеек). Имя — смысл, номер ячейки — синтаксис. Добавили имена — получили семантику. Потом метки вместо адресов. Тоже семантика. Потом функции. Имя? Семантика! Лямбды? Минус семантика.

                              Дальше — группа функций и переменных, делающих что-то. Модуль. имя? Семантика.
                              Группа переменных? Структура. Имя? Семантика! И дальше по той же линии. Классы, трейты, синтаксические макросы, typeclass'ы, namespace'ы, lifetime'ы. Каждый раз, когда мы что-то новое называем — это улучшает понимание кода. Но мы не можем называть те штуки, которые не уменьшают его размер. Это означает, что нам нужен всё более и более выразительный синтаксис. Как только синтаксис уменьшает размер кода, код становится выразительнее. Как только синтаксис требует идентификаторов, код становится понятнее.

                              Направление развития языков программирования — выразительность синтаксиса, подкрепляющаяся семантикой именования.
                                0
                                Тут главное, именовать так, чтобы понятней было, а не наоборот. Вот в посте упомянуто про Model и Controller. На практике 91,35456(6)% знакомых разработчиков видя class UserModel или class User extends Model думают не о модели в смысле MVC, не о модели в смысле объектной модели какой-то сущности предметной области, а о де-факто RowTableGateway «де-юре» именующемся ActiveRecord.
                                  0
                                  На мой взгляд еще очень важно, кто и для чего работает с кодом:
                                  а. Разработчик, который хорошо знаком с кодом — он его автор, он знает суть деталей — для него нет цикла по переменной — он знает что делает целиком этот блок for. А вот эта лябда тут и так понятно для чего.
                                  б. Разработчик, который только использует этот код и он например «почему то не работает». Он не понимает что может делать этот for c итерацией по i. А зачем тут лямбда и что она должна делать.
                                  в. Разработчик, использующий код как библиотеку — он вообще в потроха не полезет, ему куда важнее насколько адекватен интерфейс метода или коментарии в заголовочнике.

                                  И у вы на практике — это работает только когда все идеально — и код, и коментарии, и пространство имен, и общепринятый coding style.
                                  И часто ситуация промежуточная — код выглядит читаемым и понятным, но по закону Мерфи существенные моменты, где то закопаны в дебрях.

                                  В итоге вот эта грань между семантикой и длинной кода очень тонка. Да лично я предпочту
                                  for ( namespace::listofmyclass::iterator it=....) вместо for (auto: a… Но это просто потому что часто приходится искать именно дефекты серии «хотели сделать вот так а получилось вот эдак)
                                    0
                                    Не бывает типа «а». Чем больше проект, тем меньше процент того, что любой из разработчиков может удержать в голове. Если в проекте (библиотеке) хотя бы 5-7 тысяч строк, то даже автор этих строк всего не удержит. (Я говорю по своему опыту, и думаю, что нейрофизиология у меня не сильно от других отличается).

                                    И вы не описали главного пользователя языков программирования, программиста, который открыл этот код для ознакомления. Чтобы что-то поправить, дописать, поменять, улучшить или просто разобраться что происходит.

                                    Именно для читателей и требуется семантика. Для писателей требуется синтаксис. А вот читателю (и писатель тоже читатель после некоторого числа строк) — нужна семантика.
                                      0
                                      Вот как раз бывает тип а. И именно в больших проектах… Сидит вот такой гуру, врос корнями в стул и пилит «мою прелесть» — библиотечку из 3-4-10 файликов. Вы может и не сможете библиотеку из 5-7 тысяч строчек кода знать — у вас она тупо не одна… а такой вот автор — запросто, это плод всей его жизни может быть.

                                      А пользователя про «изменить» это тип б. «по классификации FYR» )))

                                      Я все таки за важность баланса между семантикой и объемом кода… При большом объеме даже понятная семантика приводит к тому что упускаешь моменты. Отсюда сначала «стандартные библиотеки», потом и паттерны проектирования, а потом и целые фреймворки вылезают — такие своеобразные надстройки над языком.
                                      Сказали тебе абстрактная фабрика — значит абстрактная фабрика и незачем тебе в семантику реализации читать.
                                        0
                                        Ну не бывает таких. Он может знать про какие-то конкретные места (которые часто меняет и которые находятся в фокусе внимания), но любой код состоит не только из «горячих» мест. Там много другого кода (те же тесты), в котором «написал и забыл». Вот чтобы «не забыть», вспомнить, в том числе спустя [выходные/месяц/год] и нужна семантика.

                                        Почему я уверен, что не бывает? Потому что у человека ограниченная память. Если человек помнит сложные места, это хорошо. Но как насчёт всего остального? У любой библиотеки будут скучные места, где всё хорошо и тривиально. Парсинг аргументов, работа с памятью, тривиальная обработка ошибок и т.д.

                                        В программистов с идеальной «фотографической памятью» я не верю. Им думать надо, а не заучивать наизусть собственный код.
                                          0
                                          Собственно «абстрактная фабрика» — это и есть чистая семантика, не поддержаная синтаксисом ЯП.
                                    0

                                    Пример про состояние переменной считаю некорректным, т.к. там сравниваются разные функции.
                                    Кроме того, вычисление НОД через хвостовую рекурсию, на мой взгляд, более понятная реализация, чем циклом. По крайней мере она 1 в 1 отражает запись НОД(a,b) = HОД(b, a%b).

                                      0
                                      Спасибо автору Владимиру tangro за интересный материал.
                                      Жаль, что автор ограничился текстовым программированием и не рассмотрел визуальное.

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое