По мнению Артема Закируллина*, одна из фундаментальных проблем, с которой сталкиваются разработчики при анализе кода – высокая когнитивная нагрузка. Это не абстрактное, а реальное ограничение возможностей, которое стоит времени и денег. На чтение и понимание кода, тратится больше времени, чем на его написание. Поэтому, разработчику нужно постоянно задаваться вопросом: не пишет ли он код, чтение которого создает чрезмерную когнитивную нагрузку?

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

*Обращаем ваше внимание, что позиция автора может не всегда совпадать с мнением МойОфис.


Что такое когнитивная нагрузка

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

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

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

Изображение было переведено*. Лицензия
Изображение было переведено*. Лицензия

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

Знакомо или просто?

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

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

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

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

Благодарю Дэна Норта за его комментарий выше.

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

Типы когнитивной нагрузки

Внутренняя — вызвана изначальной сложностью задачи. Ее нельзя уменьшить, поскольку она является самой сутью разработки ПО.

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

Давайте рассмотрим конкретные примеры внешней когнитивной нагрузки.

P.S.: Обратная связь приветствуется!


Уровень нагрузки будем обозначать так:

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

?++: два факта в нашей рабочей памяти, повышенная нагрузка

?: переполнение рабочей памяти, более четырех фактов

Сложные условные операторы

if val > someConstant // ?+
    && (condition2 || condition3) // ?+++, предыдущее условие должно быть верным, одно из условий c2 или c3 должно выполняться
    && (condition4 && !condition5) { // ?, всё, потеряли нить
    ...
}

Вводите промежуточные переменные с понятными именами:

isValid = var > someConstant
isAllowed = condition2 || condition3
isSecure = condition4 && !condition5 
// ? , нам не нужно запоминать условия, благодаря описательным переменным
if isValid && isAllowed && isSecure {
    ...
}

Вложенные конструкции if

if isValid { // ?+, ладно, вложенный код применим только к валидному вводу
    if isSecure { // ?++, выполняем действия только для валидного и безопасного ввода
        // ?+++
    }
} 

Сравните это с предыдущими returns:

if !isValid
    return
 
if !isSecure
    return

// ?, на самом деле нас не волнуют предыдущие return, если мы здесь, то все хорошо

// ?+

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

Кошмар наследования

Нам нужно кое-что поменять для администраторов: ?

AdminController наследуется от UserController, который наследуется от GuestController, который наследуется от BaseController

Ого, часть функциональности находится в BaseController, смотрим: ?+

В GuestController внедрена базовая механика ролей: ?++

Кое-что частично изменено в UserController: ?+++

Вот мы и добрались, AdminController, пишем код! ?++++

Подождите, есть еще SuperuserController который наследуется от AdminController. Изменяя AdminController, мы можем сломать что-нибудь в наследуемом классе, поэтому сначала посмотрим SuperuserController: ?

Предпочитайте композицию наследованию. Не будем углубляться в детали — информации и так предостаточно.

Слишком много мелких методов, классов или модулей

Метод, класс и модуль в этом контексте взаимозаменяемы.

Оказалось, что такие мантры, как «методы должны быть короче 15 строк кода» или «классы должны быть маленькими», не совсем верны.

Глубокий модуль: простой интерфейс, сложная функциональность

Мелкий модуль: относительно сложный интерфейс с небольшой функциональностью, которую он предоставляет.

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

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

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

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

Лучшие компоненты — это те, которые обеспечивают мощную функциональность при простом интерфейсе.

Джон К. Оустерхаут

Интерфейс ввода-вывода UNIX очень простой. В нем всего пять основных вызовов:

open(path, flags, permissions)
read(fd, buffer, count)
write(fd, buffer, count)
lseek(fd, offset, referencePosition)
close(fd)

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

Этот пример глубокого модуля взят из книги Джона К. Оустерхаута «Философия разработки программного обеспечения». Книга не только раскрывает суть сложности разработки программного обеспечения, но и предлагает лучшую интерпретацию значимой работы Парнаса «О критериях для декомпозиции систем на модули». Обе книги стоит прочитать. Что еще почитать: «Вероятно, хватит рекомендовать "Чистый код"», «Вредные маленькие функции», «Линейный код более читаем».

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

Слишком много неглубоких микросервисов

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

Однажды я консультировал стартап, где команда из трех разработчиков внедрила 17(!) микросервисов. Они отставали от графика на десять месяцев и, похоже, были далеки от публичного релиза. Каждое новое требование приводило к изменениям более чем в четырех микросервисах. Сложность диагностики в интеграции резко возросла. И время выхода на рынок, и когнитивная нагрузка были неприемлемо высокими. ?

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

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

Языки программирования с обилием функций

Нас всегда радуют новые функции в наших любимых языках программирования. Мы тратим время на их изучение и строим код на их основе.

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

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

Эти слова принадлежат Робу Пайку.

Снижайте когнитивную нагрузку, ограничивая количество вариантов.

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

Мысли инженера с 20+ летним опытом работы с C++ ⭐️

На днях я просматривал свою RSS-ленту и заметил, что у меня скопилось около трехсот непрочитанных статей по тегу «C++». Я не читал ни одной статьи об этом языке с прошлого лета, и прекрасно себя чувствую!

Уже 20 лет я программирую на C++, это почти две трети моей жизни. Большая часть моего опыта связана с самыми темными закоулками этого языка (такими как неопределенное поведение всех видов). Этот опыт сложно применить где-то еще, но и отбрасывать его полностью не хочется.

Представьте себе, например, что токен || имеет разное значение в requires ((!P<T> || !Q<T>)) и в requires (!(P<T> || Q<T>)). В первом случае это дизъюнкция ограничений, во втором — старый добрый логический оператор ИЛИ, и они ведут себя по-разному.

Раньше в C++ нельзя было просто выделить память под тривиальный тип и скопировать туда набор байтов с помощью memcpy  — это не инициализировало объект. И так было до появления C++20, где это исправили, но когнитивная нагрузка языка только выросла.

Нагрузка растет постоянно, даже несмотря на исправления. Я должен знать, что было исправлено, когда и каким образом это работало раньше. В конце концов, я профессионал. Конечно, C++ хорошо поддерживает устаревший код, а это значит, что вам с ним придется столкнуться. Например, в прошлом месяце коллега спрашивал меня об определенном аспекте работы C++03. ?

Существовало 20 способов инициализации. В обновлённой версии добавлен синтаксис единообразной инициализации. Теперь у нас есть 21 способ. Кстати, кто-нибудь помнит правила выбора конструкторов из списка инициализации? Что-то про неявное преобразование с наименьшей потерей информации, но если значение известно статически, тогда... ?

Эта возросшая когнитивная нагрузка не связана с текущей бизнес-задачей. Это не внутренняя сложность предметной области. Это – историческое наследие (внешняя когнитивная нагрузка).

Мне пришлось придумать правила. Например, если строка кода не очевидна, и мне нужно помнить стандарт, лучше ее так не писать. Кстати, стандарт насчитывает около 1500 страниц.

Я ни в коем случае не хочу обвинять C++. Я люблю этот язык.

Бизнес-логика и коды состояния HTTP

На бэкенде мы возвращаем:

401 для истекшего JWT-токена

403 для недостатка прав доступа

418 для заблокированных пользователей

Разработчики на фронтенде используют API бэкенда для реализации функциональности входа в систему. Им пришлось бы временно создать в своей голове следующую когнитивную нагрузку:

401 истекший JWT-токен // ?+, ладно, временно запомнить

403 для недостатка прав доступа // ?++

418 для заблокированных пользователей // ?+++

Фронтенд-разработчики (хотелось бы надеяться) ввели бы переменные/функции вроде isTokenExpired(status), чтобы последующим поколениям разработчиков не приходилось заново создавать в голове это сопоставление статус -> значение.

Затем в игру вступают специалисты по QA-тестированию: «Я получил статус 403. Это истекший токен или у меня недостаточно прав»? Сотрудники QA не могут сразу перейти к тестированию, потому что сначала им нужно воссоздать когнитивную нагрузку, которую когда-то создали люди на бэкенде.

Зачем держать это пользовательское сопоставление в рабочей памяти? Лучше абстрагировать детали бизнес-логики от протокола передачи HTTP и возвращать самоописывающие коды непосредственно в теле ответа:

{
    "code": "jwt_has_expired"
}

Когнитивная нагрузка со стороны фронтенда: ? (свежая, факты не удерживаются в памяти)

Когнитивная нагрузка со стороны QA: ?

Это правило применимо ко всем видам числовых статусов (в базе данных или где-либо еще) — отдавайте предпочтение самоописывающим строкам. Мы уже не живем во времена компьютеров с 640 КБ памяти, чтобы оптимизировать хранилище.

Люди тратят время на споры между статусами 401 и 403, делая выбор, исходя из своего уровня понимания. Но в итоге это абсолютно бессмысленно. Мы можем разделить ошибки на связанные с пользователем или с сервером, но в остальном все довольно туманно. Что касается следования таинственному RESTful API и использования всевозможных HTTP-методов и статусов, то стандарта попросту не существует. Единственный достоверный документ по этому вопросу — статья Роя Филдинга, опубликованная еще в 2000 году, и в ней ничего не говорится о методах и статусах. Люди прекрасно справляются, используя несколько базовых HTTP-статусов и только методом POST.

Злоупотребление принципом DRY (Don't Repeat Yourself)

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

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

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

Роб Пайк однажды сказал:

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

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

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

Тесная интеграция с фреймворком

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

Слишком сильно полагаясь на фреймворк, мы заставляем всех последующих разработчиков сначала изучать его (или его конкретную версию). Хотя фреймворки дают возможность запускать MVP за считанные дни, в долгосрочной перспективе они, как правило, добавляют ненужную сложность и когнитивную нагрузку.

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

Мы ни в коем случае не призываем изобретать все с нуля!

Мы можем писать код независимо от конкретного фреймворка. Бизнес-логика не должна находиться внутри фреймворка, а должна использовать его компоненты. Размещайте фреймворк вне ядра вашей логики. Используйте фреймворк как библиотеку. Тогда новые участники проекта будут приносить пользу с первого дня, и им не нужно будет сначала пробираться через дебри связанных с фреймворком сложностей.

Гексагональная/Луковая архитектура

Вокруг всей этой темы существует определенный инженерный ажиотаж.

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

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

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

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

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

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

  • принцип инверсии зависимостей; 

  • изоляция; 

  • единый источник истины; 

  • истинная инвариантность; 

  • сложность; 

  • когнитивная нагрузка и скрытие информации.

DDD

У доменно-ориентированного проектирования множество преимуществ, хотя часто его трактуют неправильно. Люди говорят: «Мы пишем код по DDD», что странно, потому что DDD фокусируется на пространстве проблем, а не решений.

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

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

Учимся у ИТ-гигантов

Вот основные принципы проектирования одной из крупнейших технологических компаний:

  • ясность: цель и обоснование кода понятны читающему.

  • простота: код достигает своей цели наиболее простым способом.

  • лаконичность: в коде легко различить важные детали, а именование и структура помогают читающему в них ориентироваться.

  • поддерживаемость: следующему программисту должно быть легко правильно изменять код.

  • Согласованность: код согласован с остальной кодовой базой.

Соответствует ли новый модный термин этим принципам? Или это лишь создает излишнюю когнитивную нагрузку?

Вот забавная картинка

Знакомо или просто? Автор: @flaviocopes
Знакомо или просто? Автор: @flaviocopes

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

Брайан Керниган

Заключение

Многогранная и сложная природа когнитивной нагрузки в области понимания и решения проблем требует тщательного и стратегического подхода для преодоления сложностей и оптимального распределения умственных способностей. ?

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

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

*"UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT  ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU".

Комментарии (11)


  1. raamid
    31.05.2024 15:21
    +2

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

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

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

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


  1. kozlov_de
    31.05.2024 15:21
    +2

    не пишите write only code


  1. dyadyaSerezha
    31.05.2024 15:21
    +2

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


  1. mvv-rus
    31.05.2024 15:21
    +2

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

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

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

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

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

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


    1. antirek
      31.05.2024 15:21

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

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

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

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

      из а) идет б)

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

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

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

      далее г)

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

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

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

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

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


  1. Anurath
    31.05.2024 15:21
    +3

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

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

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

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

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


  1. AtariSMN82
    31.05.2024 15:21
    +2

    нет кода, нет проблем


  1. kozlov_de
    31.05.2024 15:21

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

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


  1. Andruh
    31.05.2024 15:21

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

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

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

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


  1. boldape
    31.05.2024 15:21

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

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

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

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

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

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

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

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

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

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

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

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

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


  1. kasiopei
    31.05.2024 15:21

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

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