Важна именно когнитивная нагрузка.

Введение

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

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

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

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

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

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

Нам следует максимально снизить когнитивную нагрузку в наших проектах. Когнитивная нагрузка и прерывания

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

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

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

Умный автор

Сложные условные предложения

if val > someConstant // 🧠+
    && (condition2 || condition3) // 🧠+++, prev cond should be true, one of c2 or c3 has be true
    && (condition4 && !condition5) { // 🤯, we are messed up by this point
    ...
}

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

isValid = val > someConstant
isAllowed = condition2 || condition3
isSecure = condition4 && !condition5
// 🧠, we don't need to remember the conditions, there are descriptive variables
if isValid && isAllowed && isSecure {
    ...
}

Вложенные if-ы

if isValid { // 🧠+, okay nested code applies to valid input only
    if isSecure { // 🧠++, we do stuff for valid and secure input only
        stuff // 🧠+++
    }
}

Сравните это с ранними результатами:

if !isValid
    return

if !isSecure
    return

// 🧠, we don't really care about earlier returns, if we are here then all good

stuff // 🧠+

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

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

Нас просят изменить несколько вещей для наших пользователей-администраторов:🧠

AdminController extends UserController extends GuestController extends BaseController

О, часть функционала есть в BaseController, давайте посмотрим: 🧠+
Базовая ролевая механика была введена в GuestController🧠++
Частично изменения произошли в UserController🧠+++
Наконец-то мы здесь, AdminControllerдавайте писать код!🧠++++

Ой, подождите, есть SuperuserControllerwhich extends AdminController. Изменяя, AdminControllerмы можем что-то сломать в унаследованном классе, так что давайте SuperuserControllerсначала разберемся:🤯 Предпочитать композицию наследованию. Не будем вдаваться в подробности — материала предостаточно .

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

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

Глубокий модуль

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

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

Интерфейс UNIX I/O очень прост. Он имеет всего пять основных вызовов:

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

Современная реализация этого интерфейса имеет сотни тысяч строк кода . Под капотом скрывается много сложности. Тем не менее, он прост в использовании благодаря простому интерфейсу. Этот пример глубокого модуля взят из книги «Философия проектирования программного обеспечения» Джона К. Оустерхаута.

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

Этот принцип мелко-глубокого модуля не зависит от масштаба, и мы можем применить его к архитектуре микросервисов. Слишком много мелких микросервисов не принесут никакой пользы — отрасль движется к некоторым «макросервисам», т. е. сервисам, которые не столь поверхностны (=глубоки). Однажды я консультировал стартап, где команда из пяти разработчиков представила 17(!) микросервисов. Они отставали от графика на 10 месяцев и даже близко не подошли к публичному релизу. Каждое новое требование приводило к изменениям в 4+ микросервисах. Диагностическая сложность в области интеграции резко возросла. И время выхода на рынок, и когнитивная нагрузка были неприемлемо высокими.🤯 Правильный ли это способ подхода к неопределенности новой системы? Чрезвычайно сложно выявить правильные логические границы в начале. Главное — принимать решения как можно позже, ответственно ожидая, потому что именно тогда у вас будет больше всего информации, на которой можно основывать решение. Вводя сетевой уровень заранее, мы делаем наши проектные решения трудноотменяемыми с самого начала. Единственным оправданием команды было: «Компании FAANG доказали эффективность архитектуры микросервисов». Привет, вам пора перестать мечтать о большем.

В дебатах Таненбаума -Торвальдса утверждалось, что монолитный дизайн Linux был несовершенным и устаревшим, и что вместо него следует использовать микроядерную архитектуру. Действительно, микроядерный дизайн казался более совершенным «с теоретической и эстетической» точки зрения. С практической стороны вещей — три десятилетия спустя GNU Hurd на основе микроядра все еще находится в разработке, а монолитный Linux повсюду. Эта страница работает на Linux, ваш умный чайник работает на Linux. Монолитным Linux. Хорошо продуманный монолит с действительно изолированными модулями часто гораздо более гибок, чем куча микросервисов. Он также требует гораздо меньше когнитивных усилий для поддержки. Только когда необходимость в отдельных развертываниях становится критической, например, при масштабировании команды разработчиков, следует рассмотреть возможность добавления сетевого уровня между модулями, будущими микросервисами.

Многофункциональные языки

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

Вам нужно не только понять эту сложную программу, но и понять, почему программист решил, что это именно тот способ решения проблемы, который доступен. 🤯 Эти заявления сделал не кто иной, как Роб Пайк. Уменьшите когнитивную нагрузку, ограничив количество вариантов выбора. Языковые особенности допустимы, если они ортогональны друг другу. Мысли инженера с 20-летним опытом работы с C++ ⭐️

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

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

  • 401для истекшего токена jwt
  • 403за недостаточный доступ
  • 418для забаненных пользователей

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

  • 401для истекшего токена jwt // 🧠+, хорошо, просто временно запомните его
  • 403для недостаточного доступа //🧠++
  • 418для забаненных пользователей //🧠+++

Разработчики интерфейса (надеюсь) введут на своей стороне некий словарь числового статуса -> значения , чтобы последующим поколениям участников не пришлось заново создавать это сопоставление в своих мозгах. Затем в игру вступают инженеры по контролю качества: «Эй, я получил 403статус, это просроченный токен или недостаточно прав доступа?» Инженеры по контролю качества не могут сразу перейти к тестированию, потому что сначала им нужно воссоздать когнитивную нагрузку, которую инженеры на бэкэнде когда-то создали. Зачем хранить это пользовательское сопоставление в нашей рабочей памяти? Лучше абстрагировать данные вашего бизнеса от протокола передачи HTTP и возвращать самоописательные коды непосредственно в теле ответа:

{
    "code": "jwt_has_expired"
}

Когнитивная нагрузка на стороне frontend: 🧠(свежее, никакие факты не удерживаются в памяти)
Когнитивная нагрузка на стороне QA:🧠 То же правило применимо ко всем видам числовых статусов (в базе данных или где-либо еще) — предпочитайте самоописывающие строки . Мы не в эпохе компьютеров 640К для оптимизации памяти.

Тесная связь с каркасом

В фреймворках много «магии». Слишком сильно полагаясь на фреймворк, мы заставляем всех будущих разработчиков сначала изучать эту «магию» . Это может занять месяцы. Несмотря на то, что фреймворки позволяют нам запускать MVP в течение нескольких дней, в долгосрочной перспективе они, как правило, добавляют ненужную сложность и когнитивную нагрузку. Хуже того, в какой-то момент фреймворки могут стать существенным ограничением, когда сталкиваются с новым требованием, которое просто не соответствует архитектуре. С этого момента люди в конечном итоге разветвляют фреймворк и поддерживают свою собственную версию. Представьте себе объем когнитивной нагрузки, которую должен был бы создать новичок (т. е. изучить этот пользовательский фреймворк), чтобы предоставить какую-либо ценность.🤯

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

Почему я ненавижу фреймворки

Многоуровневая архитектура

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

Если вы думаете, что такое разделение на слои позволит вам быстро заменить базу данных или другие зависимости, вы ошибаетесь. Изменение хранилища вызывает множество проблем, и поверьте нам, наличие некоторых абстракций для уровня доступа к данным — это наименьшая из ваших забот. В лучшем случае абстракции могут сэкономить около 10% времени миграции (если вообще это нужно), настоящая боль заключается в несовместимости моделей данных, протоколах связи, проблемах распределенных систем и неявных интерфейсах . Мы выполнили миграцию хранилища, и это заняло у нас около 10 месяцев. Старая система была однопоточной, поэтому выставленные события были последовательными. Все наши системы зависели от этого наблюдаемого поведения. Это поведение не было частью контракта API, оно не было отражено в коде. Новое распределенное хранилище не имело такой гарантии — события приходили не по порядку. Мы потратили всего несколько часов на кодирование нового адаптера хранилища благодаря абстракции. Следующие 10 месяцев мы потратили на решение не по порядку событий и других проблем. Теперь смешно говорить, что абстракции помогают нам быстро заменять компоненты.

Так зачем же платить высокую когнитивную нагрузку за такую ​​многоуровневую архитектуру, если она не окупится в будущем? Эти архитектуры не являются фундаментальными, они просто субъективные, предвзятые следствия более фундаментальных принципов. Зачем полагаться на эти субъективные интерпретации? Вместо этого следуйте фундаментальным правилам: принцип инверсии зависимостей, единый источник истины, когнитивная нагрузка и сокрытие информации. Ваша бизнес-логика не должна зависеть от низкоуровневых модулей, таких как база данных, пользовательский интерфейс или фреймворк. Мы должны иметь возможность писать тесты для нашей основной логики, не беспокоясь об инфраструктуре, и все. Обсудить . Не добавляйте слои абстракций ради архитектуры. Добавляйте их всякий раз, когда вам нужна точка расширения, оправданная по практическим соображениям.

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

Слои

Примеры

  • Наша архитектура — это стандартная архитектура CRUD-приложений, монолит Python поверх Postgres.
  • Как Instagram масштабировался до 14 миллионов пользователей с помощью всего 3 инженеров
  • Компании, о которых мы говорили: «Ого, эти ребята чертовски умны », по большей части терпели неудачу.
  • Одна функция, которая связывает всю систему. Если вы хотите узнать, как работает система — прочитайте ее

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

Когнитивная нагрузка в знакомых проектах

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

Вероятно, предыдущий автор(ы) создавал(ли) этот огромный беспорядок по одному крошечному шагу за раз, а не все сразу. Так что вы первый человек, которому когда-либо приходилось пытаться осмыслить все это сразу. На своем занятии я описываю разросшуюся хранимую процедуру SQL, которую мы рассматривали однажды, с сотнями строк условий в огромном предложении WHERE. Кто-то спросил, как кто-то мог допустить, чтобы все стало так плохо. Я сказал им: «Когда есть только 2 или 3 условия, добавление еще одного ничего не меняет. Когда есть 20 или 30 условий, добавление еще одного ничего не меняет!» Нет никакой «упрощающей силы», действующей на кодовую базу, кроме осознанного выбора, который вы делаете. Упрощение требует усилий, а люди слишком часто торопятся.

Спасибо Дэну Норту за его комментарий .

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

Ментальные модели

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

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

Заключение

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

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

Умный автор

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


Артем Закируллин