Pull to refresh

Comments 64

Здорово когда простым языком объяснят то, что воспринимаешь интуитивно. Теперь можно не просто сказать «так надо», а «так надо, потому что твой мозговой процессор так работает»
Очень годно, думаю к аналогичным выводам приходят многие программисты спустя 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.
Так код и есть воплощение ментальной модели!
Но как «красота в глазах смотрящих», так и понимание ментальной модели зависит от читающего.
А вот что он увидит…
И никак не проконтролируешь.
Процесс передачи понимания асинхронный.
Поэтому мне нравиться концепция ТДД.
Как минимум машине проще объяснить чего ты хочешь, чем человеку. :-)
Одним из примеров может служать концепция раннего возврата («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 {
        // сделать что-то
    }
}

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

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

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

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

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

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

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

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

Задача «добавить код» сама по себе в принципе не возникает.
Как и задача писать код сама по себе в принципе не возникает. Но мы же и пишем, и добавляем?
Вот я и постарался придумать в том контексте: 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 будет неуместен.
В приведенном вами коде 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? Вы действительно прыгаете, затрачивая энергию, а не читаете наглядное ветвление? Может, с непривычки?
Если не стоит запрет на преждевременный выход и я привык его применять, это не увеличит нагрузку на распознавание ситуации, что дальше есть необходимые действия и так поступать нельзя?
Безусловно увеличит. Но, как я уже сказал, проблема возникает лишь в тех средах, где нет возможности использовать 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
Не совсем понял вопрос. В любом случае, этот вариант меня так же устраивает (за исключением опечаток)
Дело именно в «опечатках». Статья, напомню, о психологии читабельности, и Ваш комментарий о более лёгком сопровождении.

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

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

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

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

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

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

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

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

Если в коде встречается виртуальная лесенка из 10 return (а не switch), то скорее всего, там проблема на архитектурном уровне
То же самое можно сказать о двух goto в коде Apple, да и в принципе о любой проблеме. У вас получается нефальсифицируемая теория: любой недостаток вы списываете на внешние причины. Моя теория работает везде, а там где она не работает — это у вас плохая архитектура. Неконструктивно.
Стоп, а почему вы решили что не может? На мой взгляд все ровно наоборот: в вашем примере заметить опечатку гораздо сложнее
А Вы попробуйте 1. совершить эту ошибку. 2. не заметить сместившийся блок. 3. Сопоставьте это с изменением потока управления из-за наличия или отсутствия линейного оператора.
То же самое можно сказать о двух goto в коде Apple
О том и речь, и это напрямую связано с наличием неструктурных переходов.
У вас получается нефальсифицируемая теория
Какая теория? О том, что в заданном вопросе недостаточно информации, для универсального ответа? Или Вас удивляет, что наличие «потребности» в большом количестве неструктурных переходов свидетельствует о проблемах проектирования? Об этом пишут ещё с 70-х. Вы знакомы с классикой Дейкстры? Знакомы с рекомендациями MISRA C для критического к корректности ПО и им подобным? Моя «нефальсифицируемая теория» написана практикой задолго до меня. Но сообщество, к сожалению, ходит по кругу.
А Вы попробуйте
Попробовал, получилось довольно легко, т.к. код выглядит валидно. В обоих вариантах это выглядит не как 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
Представьте себе, знаком. Вот только сейчас не 70-е
Зачем мне представлять? Если мой риторический вопрос заключался в указании на то, что это не у меня получается нефальсифицируемая теория, а написана она уже давно на основе практики. То, что о ней пишут с 70-х это не означает, что эти аргументы устарели.
При фанатичном следовании «классике» Дейкстры придется отказаться не только от multiple return, но и от break/continue в циклах, и от try/catch/finally в языках с исключениями. К счастью, фанатиков нынче не много, да и сам Дейкстра сегодня вряд ли бы поддержал фанатиков.
Если Вы пишите об этом как о фанатизме, и даже делаете предположения о поддержке со стороны Дейкстры, то это значит, что Вы, как минимум, невнимательно отнеслись к его трудам.
А вас не смущает что 'эти рекомендации официально допускают отклонения? И что они написаны для конкретной индустрии (embedded), конкретно для С (где памятью приходится управлять вручную)
Вы незнакомы с MISRA C, иначе бы не стали ничего писать про выделение памяти вручную. А также знали бы, что лишь часть этих рекомендаций специфично для встроенных применений. Единственный выход не относится к ним, а вот неиспользование динамически распределяемой памяти — относится.

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

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

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

Следовательно, следуя заветам Дейкстры, следует отказаться от раннего возврата. Однако, с тех пор: 1. появилось много языков с автоматическим освобождением ресурсов.
Во-первых, Вы путаете заветы и доводы. Во-вторых, освобождение ресурсов — это лишь часть ситуаций, где важно соблюдать порядок. Ранее я давал ссылку на эпичный промах Apple, где не было проблемы с освобождением ресурсов, но была проблема с неструктурными переходами. Вы не заметили этого или не придали значения, наверно, отчасти поэтому написали это:
я не делаю упор на «я так вижу».
вы делаете упор именно на это.
Интерпретация чужих слов — это, во многом, вопрос выбора. Ваш выбор таков, это нормально.
что важнее — факт существования или относительное количество?
Важно всё. Даже то, каким образом получены данные о количестве. Если методом опроса, то у меня есть плохие новости, если по серии экспериментов и наблюдений, то совсем другое дело.
исследование по поводу эффективности 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, менее надежен.
И вообще, вы называете «неструктурным» код, в котором структура прослеживается проще.
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 С, ссылающихся на них люди умудряются прийти к прямо противоположным выводам, не читая самого исследования и не понимая к чему относятся слова из цитаты в Википедии.
И вообще, вы называете «неструктурным» код, в котором структура прослеживается проще.
Я пользуюсь общепринятой терминологией, которую, возможно, Вам тоже следует изучить прежде, чем что-то брать в кавычки.
Я пользуюсь общепринятой терминологией, которую, возможно, Вам тоже следует изучить прежде, чем что-то брать в кавычки.

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

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

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

да.
Не знаю, почему Вам показалось, что эти языки были приведены для доказательства рекомендаций, появившихся до 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.

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

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

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

Общепринятая трактовка в данном случае не является оригинальной. И вы бы об этом знали, следуй вы собственным советам
Загляните ещё в английскую Википедию
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.
Это они чушь какую-то написали, не посоветовавшись с Вами?
То, что о ней пишут с 70-х это не означает, что эти аргументы устарели.
В 70-з не было аргумента «если мои советы вам не подходят, то у вас говнокод». Это ваше новшество.

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

Ну и напоследок, как Вы думаете, всё-таки есть люди, для которых структурный поток более читаемый, или это непременно фанатики из 70-х?
Зависит то того, что вы здесь подразумеваете под структурным потоком. Если речь только о single return в функциях, то конечно есть, это во многом дело вкуса. Если же полный отказ от break/continue в циклах и try/catch/finally — то это, безусловно, фанатизм.
В 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 — то это, безусловно, фанатизм.
Я просто хочу обратить внимание на то, что немногим ранее Вы обвинили меня в том, что я будто-бы утверждаю «если мои советы вам не подходят, то у вас говнокод». А теперь заявляете, что несогласные с Вами — это, безусловно, фанатики. Похоже на зеркалирование собственных поступков, нет?
UFO just landed and posted this here
Несколько раз встречал ошибки что в начинают добавлять код после закрывающего блока для 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 и диагностировать это сложней в общем случае
UFO just landed and posted this here
Про «else if» пример был для самых простых случаях. Обычно это что-то типа
Тогда нужно разбивать на шаги. Ибо иначе огромные функции так и растут. Из-за усеянности неструктурными переходами разбивать функцию становится всё сложней.

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

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

Вообще-то люди старше ста лет существуют.
Так что код так же неверно построен, как и база данных в которой у всех людей есть фамилия, имя и отчество — и каждое поле не длиннее 16 символов, никак не учитывающая то, что клиент может оказаться иностранцем.
UFO just landed and posted this here
Чего спорить то? 150 поставьте. Вопрос был в структуре, а не в примере.
С точки зрения читаемости такой код лучше:

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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