Pull to refresh

Comments 20

Хорошая статья, спасибо.

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

Дополнительный бонус - иногда такая "библиотека" со временем становится настоящей персональной библиотекой, которая кочует из проекта в проект.

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

Проблема стара, как мир, и в сети живут сотни статей по данному вопросу. Но в целом, неплохо. Главное, не переусердствовать слишком с любыми хорошими принципами или архитектурными решениями, а каждый раз применять их вдумчиво и обоснованно.

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

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

Что до примеров из статьи, то пример не очень работающего способа нашелся прямо в самом начале. Вот смотрю я на этот "Сложный условный оператор" и введение промежуточных локальных переменных для его упрощения и думаю: ведь нет тут никакой особой нагрузки, ради того чтобы стоило стараться ее снижать. Ибо тренированный разработчик все эти три локальные переменные с их особыми смыслами с легкостью соберет в своей голове (тот самый обещанный пример уменьшения числа сущностей). Но зато ему не придется писать лишних ,не просто букв, но строк текста - а ведь каждая строка текста - это выталкивание какого-то кода за пределы поля зрения - физического, обусловленного свойствами монитора и глаз, что когнитивную сложность как раз, наоборот, повышает. А ещё - ничто при дальнейшей модификации кода не сможет вклиниться между присвоением этих переменных и проверкой условий.

Способ снижения сложности заменой вложенных if на ранние return тоже сомнителен. Потому что он коварен: эти ранние return можно не заметить вовремя. И если потребуется дописать в конце какой-то общий код, их легко упустить. На заметку теоретикам: по факту, эти return - ничто иное, как скрытые goto (они тоже нарушают естестественную связь между последовательностью написания и последовательностью выполнения), а теоретикам еще Дейкстра заповедовал "GOTO считать вредным". Есть, конечно, надежный способ ничего не упустить - конструкция try ... finally, но, во-первых, она есть не везде, а во-вторых - предназначена для решения другой задачи и может оказаться излишне тяжелой в процессе выполнения.

А про наследование стоило бы написать отдельную статью. У меня даже заголовок для нее есть: "Вы не любите наследование? Да вы просто не умеете его готовить!" (это - перефразировка известнойи довольно старой уже фразы Альфа про кошек). Основная мысль: наследование - это специфическая форма композиции, хорошая для случаев, когда существует сильная внутренняя связанность (cohesion) между вмещающим классом и потенциальными классами компонентов. Вот тогда вместо того, чтобы делать "квазинезависимый" компонент имеет смыл унаследовать содержащий его функциональность класс от базового. Но так понимать и использовать наследование не учат. А учат его использовать на совершенно, на мой взгляд, неподходящих примерах.
К сожалению, я ленюсь (на самом деле - занимаюсь другим делом), и потому такая статья пока что не написана.

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

статья в целом и ваши рассуждения про тренировку выдержки когнитивных нагрузок - это следствия.

тут нет ответа - почему это происходит? почему разработчики пишут такой код? причины, из-за которых именно авторы кода создают высокую когнитивную нагрузку?

как по мне, если рассматривать только на уровне конкретного разработчика:

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

из а) идет б)

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

из б) следует в)

в) мы поощряем в нашей культуре сложность. восхищяемся, уважаем, поощряем, награждаем тех, кто решал сложные задачи, сложные проблемы, реализовал сложные алгоритмы, построил сложные системы. все предусмотрел, подготовился, реализовал варианты, подстраховался от "неописанных" случаев в ТЗ. при этом мы должны увидеть, что решено, реализовано именно сложным путем.

далее г)

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

и если разработчик сделал задачу за 15 минут, а не три дня, и рассказывал о ней у кулера полчаса, то что он получит? вероятно, еще одну задачу и звание балабола

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

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

про наследование более развернуто - я бы почитал

почему разработчики пишут такой код? причины, из-за которых именно авторы кода создают высокую когнитивную нагрузку?

Вы рассматриваете только одну причину, и не факт - что главную. Главная причина, на мой взгляд - в том, что когда программист создает программу, он находится в процессе решения, а не записывает уже готовое решение. Естественно, это относится только к сколь-нибудь нетривиальным программам, а не к воплощению чего-нибудь по давно известному образцу, типа формошлепства или перекладывания джейсонов. Поэтому он записывает промежуточное решение, а потом дополняет его, чтобы оно таки решало задачу. Отсюда, в основном, и возникает избыточная, не обусловленная задачей сложность программы. А названное вами стремление предусмотреть всё (over-engineering) - это причина вторичная, не всегда присутствующая - к примеру, сложность программы, полной "обходных решений" (в терминологии ITSM, по жизни их называют костылями), никак не вызвана стремлением что-нибудь предусмотреть.

А понижать сложностьпрограммы после ее написания - это отдельная работа, в которой наемный программист, кстати, совсем не заинтересован.

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

Тут нужно небольшольшое уточнение: чьи именно приоритеты. Нынешние "коллективы" - они состоят из индивидуальных наемных разработчиков - "гребцов" (на языке Прекрасного IT, он хорошо выражает сущность производственных отношений). Колеектив там - чисто мезаническое объединение, никаких своих интересов он не имеет. Наемные разработчики чисто объективно отчуждены от результата своего труда - этоочевидно, если вспомнить, что больше денег в результате успеха все равно получат не они, а владельцы предприятия. А потому все эти требования не соответствуют объективным интересам наемных разработчиков. И в реальности же это - приоритеты менеджеров-"погонщиков"(на том же языке).

Ели говорить про разработчиков, то у них есть ещё один способ избежать когнитивной перегрузки: тренировать свою способность выдерживать когнитивную нагрузку

Я думаю, что это невозможно. В явном виде. Вот что сказано в статье.

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

Получается что то вроде регистров, в которые что либо помещается/считывается. Понятно, что на одних регистрах не выехать и нужно задействовать внешние ресурсы по вычислению из condition1 && condition2 некий isValid, который, затем отправится в регистр, а condition1 и condition2 забудутся. Но это приведение нужно выполнить. И таких приведений в коде может быть множество. Кроме того, вместо condition1 и condition2 тут может быть намного больше условий, а значит они не поместятся в регистры, а значит придется делать несколько промежуточных действий. Так же, этот isValid - вполне ясная для нашего "процессора" команда, тогда как condition1 и тд нам неизвестны и требуют погружения в предметку в тех местах, которые просто не нужны для выполнения текущей задачи.

И да, мозг постоянно тренируется и новые связи помогают выполнять все эти промежуточные вычисления быстрее - по прямому пути.

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

И нет, промежуточные return это не то же самое что goto. Место куда нас ведет goto надо искать и исследовать, тогда как return всегда ведет нас на фрейм выше и дополнительное исследование тут не требуется. Мы ведь знаем, что вернет нам функция, по каким правилам она работает. И если в самой функции мы проблему не увидели (а это можно понять намного быстрее, в идеале, одним коротким взглядом на нее боковым зрением, если она понятно написана), значит проблема либо выше либо ниже. То есть либо ошибка в вызывающем коде, либо в одной из абстракций, на которую мы полагаемся внутри своей функции.

эти ранние return можно не заметить вовремя

Есть такое. Но эти же ранние return могут быть и внтури ифов. И там их заметить намного сложнее. А они там могут быть, потому что язык это позволяет. В scala, например, возвращается значение последнего выражения. Там не используется return. вместо этого блоки там тоже возвращают значение. val myVar = if (condition) val1 else val2. То есть условный оператор есть тернарное выражение. И это позволяет не сталкиваться с проблемами return. но там где они есть, приходится разрабатывать подходы, как их использовать наименее просто для восприятия. И наименее просто, когда они находятся на одном уровне вложенности. Более того, полезная нагрузка (логика) находится навиду, когда находится перед глазами без вложенности. То есть в идеале, мы сначла проходим все предусловия, а затем выполняем логику, которая сама по себе от этих предусловий не зависит. Таким образом мы визуально отделяем предусловия от логики, а не просто создаем дополнительные строки кода.

Много думал на тему когнитивной нагрузки, и для себя выработал несколько дополнительных мерил:

1) Число понятий, которые нужно подгрузить в голову, чтобы понять, что происходит, любого характера, от внутренних классов и функций до языковых фич и подключенных фреймворковых сущностей, используемых в коде. Стало быть, если из других частей кода уже была нужда подгрузить группу понятий, заново её подгружать человеку, работавшему с этой кодовой базой, уже будет не нужно, поэтому выгодно иметь везде однотипный код, а не лепить уникальное решение, которое теоретически на 5% быстрее в ограниченном наборе ситуаций, если нет прямых доказательств такой нужды.

2) Соответствие понятий из кода и понятий в голове, нужных для его осознания. Кроме явных, вроде классов, интерфейсов, функций и переменных, бывают неявные понятия, вроде isValid которые не вынесены в отдельный bool, как в примере из статьи, но всё равно должны быть подгружены в голову. Код, предоставлющий все нужные для его понимания понятия, с названиями, которые легко ассоциируются с сутью, читать легче и быстрее.

3) Локальность кода. Когда весь код, нужный для осознания, находится в одном и том же небольшом файле, а еще лучше, на одном экране, когнитивная нагрузка меньше, чем если он размазан по 6 разным файлам в произвольных частях кодовой базы и для подгрузки нужно постоянно прыгать по множественным перевызовам. Обёртки, в частности, приводят к этому такому негативному эффекту.

4) Отношение числа значимых строк кода к числу структурных. Значимые строки это строки кода, которые активно участвуют в работе программы: манипулирование данными, вычисления, условные выражения, циклы, вызовы внешних методов и ввод-вывод. Структурные - все остальные. Перегруженные структурными строками программы при прочих равных сложнее читать.

интересное слово вы использовали — мерило... отсылает к мере.

А вот по теме когнитивной нагрузки, мне кажется, что как авторы статьи, так и вы, в своём комментарии, одинаково не учитываете одной важной вещи — безсознательных уровней психики. И соответственно того, как взаимодействует сознание (которое действительно в обыденном состоянии удерживает 3-9 объектов и связей между ними) и безсознательные уровни психики (далее безсознательное для краткости).

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

Сознание поставило задачу — безсознательное её исполняет, ориентируясь на оценки, пожелания, запреты и разрешения даваемые с уровня сознания.

Применительно к созданию кода, программ — если в безсознательном есть соответствующие стереотипы разпознавания и стереотипы преобразования информации, а человек с уровня сознания способен поддерживать устойчивое самообладание (описанное в предыдущем абзаце: сознание ставит задачи — безсознательное исполняет) — то не важно сколь много текста в коде (конечно если там не мусор) — безсознательное воспримет, обработает и выдаст результат — в частности, и как понимание/ощущение того, где и что надо написать в коде. В том числе и в эмоциях (смысл которых может быть не доступен сразу, но осознан, если к тому приложить усилия).

Если же описанного нету. А в более тяжёлом случае — если не допускается даже мысль о возможности чего-то подобного, то попытки мизерными ресурсами сознания охватить сложные и обширные темы (в смысле множества разных взаимодействующих объектов со множеством разных возможных связей между ними) — мягко говоря обречены на провал.

Признавая выше сказанное, дальше можно рассуждать о том:

• а как способность к самообладанию (в описанном выше виде) освоить? И, следовательно, как с практикой программирования сочетать? — что, пожалуй, область психологов-практиков.

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

У меня когнитивные способности низкие, поэтому я пишу красивый код.

Правда, это тоже надоело и я ушёл в архитекторы, писать красивую архитектуру

Про когнитивную нагрузку интересно:

1. Люди деградируют: раньше удерживали 7 элементов в голове, теперь только 4 - уже норма. Эта разница очень существенна - не всё можно организовать через 7 связей (т.е. сложно построить систему классов, например, где каждый класс имеет не более 4 связей), иногда невозможно и это ведёт уже к ограничению наших выразительных возможностей - некоторые задачи мы не можем реализовать (но пока чаще поддерживать, т.к. ещё есть те кто могут).

  1. Люди деградируют: раньше слои абстракции не воспринимались как сложность, которую нужно удерживать в голове. Находясь на каждом слое абстракции ты думаешь только об одном или двух соседних (одному предоставляем интерфейс, у другого - используем интерфейс). Причём это вполне за скобками, не занимает ресурс у 4 (или 7).

Я думаю, что об этом тоже нужно говорить: программирование - сложная работа, если вы можете только 4 или 3, просто идите лесом. А то настоящим специалистам приходится подстраивается под вашу поголовную ограниченность, что ведёт к ограничению наших возможностей в конструировании ПО.

А то настоящим специалистам приходится подстраивается под вашу поголовную ограниченность, что ведёт к ограничению наших возможностей в конструировании ПО.

Зато дает возможность строить ПО силами стандартных специалистов, а не примадонн, что позволяет здорово снизить бас-фактор. Что на это скажете?

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

пока стандартных специалистов хватает для построения систем нужной сложности.

Если стандартных не хватает, то нестандартных будет, скорее всего, еще меньше доступно для реализации. Тут к верховному проектанту вопрос по распределению работ так, чтобы их и стандартные в коллективе выполнили. Поговорку "порядок бьет класс", думаю, Вы слышали? Она не на пустом месте возникла.

Не во всех областях эта поговорка применима.
Верховный проектант не всесилен. Но по сути вы невольно и выделили единственного настоящего айтишника в этой толпе.

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

Чаще всего это не работает, именно качество "стандартных" и имел в виду автор комментария. И оно катастрофически падает. Еще 5 лет назад указания типа "сделай один раз вью в базе чтобы не копировать сложные запросы по 5 раз" стандартному хватало, сейчас нужно каждый раз лекцию по RDBMS прочитать.

Идеальный код это не написанный код. Если уж идеал не достежим, то вероятно следующий уровень это код для которого у меня нет причин его читать - т.е. он просто работает так как я этого от него ожидаю.

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

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

Такой эфимерный и что важнее субъективный критерий как читаемость и та самая поддержка вообще какое то откровенное инфо циганство. Читаемый кем? Поддерживаемый кем? Без списка предусловий к читателю эти понятия не имеет никакого смысла. Нет объективного понятия легко читаемый/сопровождаемый код.

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

Как часто вы ставите скобки вокруг умножения когда рядом стоит оператор сложения? НИКОГДА. Так почему тогда так сложно выучить наизусть, как таблицу умножения, приорететы двух сраных логических операторов и не засирать код ненужными скобками?

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

Лукавая архитектура, чистый код или вообще микро/макро/монолит у вас вообще не важно, наследование/типы суммы или агрегация - да какая нахрен разница, консистентный у вас код или нет тоже по барабану, драй он солид или ещё какая модная херота - ну вы поняли, знаете вы приоритет логических операторов или нет это все третично.

Суть хорошего кода заключается в том, что бы у читателя ИСЧЕЗАЛИ причины продолжать его читать дальше/глубже как можно раньше, как вы этого достигните не имеет значения и что важнее в каждом случае разные подходы дают не постоянные результаты.

Знайте своего читателя/колегу/список обязательных знаний и используйте любой заоопарк инструментов и подходов что бы:

  • вообще НЕ писать код

  • писать такой код который просто работает так, как от него ожидается и не требует понимания как именно он работает

  • код который вы читаете становился понятным как можно раньше/выше по уровню абстракции

Очень важное значение имеет визуальная сложность. Может казаться ерундой, но за весь день съедает ресурс прилично. Все эти форматирования кода, подсветка синтаксиса, всякие верблюжьи нотации улучшают восприятие.

Иногда сразу пишу сложную формулу в виде

Sign up to leave a comment.