- Когнитивная нагрузка и перегрузка
- Читать тяжелее, чем писать
- Чтоб код был хорошим:
- И еще раз о релевантности
О когнитивной нагрузке
Бывает ли так, что читаете код и не можете его понять? Значит, вы когнитивно перегружены.
Под когнитивной нагрузкой понимается количество умственных усилий, необходимых для выполнения задачи. При чтении кода приходится запоминать сложную информацию: значения переменных и связи между ними, логика условий, циклы, состояния, структуры данных, интерфейсные взаимодействия.
Когнитивная нагрузка возрастает по мере усложнения программного кода. Обычные люди могут удерживать в кратковременной памяти не более 5-7 фрагментов информации (учеными доказано); код, содержащий бОльшее количество информации, может оказаться сложным для понимания, то есть когнитивно перегружающим.
Читать тесты сложнее, чем писать их
Когнитивная нагрузка для людей, читающих написанный вами код, зачастую выше, чем для вас самих, поскольку читающему необходимо правильно понять ваши намерения в коде — что вы имели в виду и что этот код должен делать, с вашей точки зрения.
Вспомните ситуации, когда читали чужой код и с трудом понимали его. Одна из главных причин проведения код-ревью заключается в том, чтобы дать возможность ревьюерам проверить, не вызывают ли изменения в коде слишком большой когнитивной нагрузки. Будьте добры к своим коллегам: уменьшайте им когнитивную нагрузку, стремитесь писать правильный чистый код.
Правильные практики
Ключ к снижению когнитивной нагрузки — целенаправленное упрощение кода, чтобы его легче было понять. Именно этот принцип лежит в основе многих, если не всех, практик чистого кода. Далее несколько примеров и практик от Google.
Минимизируй код
Ограничьте объем кода в функции или файле. Стремитесь к тому, чтобы код был достаточно кратким, чтобы его можно было легко запомнить, то есть удержать в кратковременной памяти. Старайтесь, чтобы функции были небольшими (об этом можно почитать здесь), и старайтесь ограничить каждый класс каким-то одним присущим ему действием.
Создавай абстракции, прячь имплементации
Создавайте абстракции, чтобы скрыть детали реализации. Такие абстракции, как функции и интерфейсы, позволяют работать с более простыми понятиями и скрывать (точнее, «свёртывать») сложные подробности. Однако помните, что чрезмерное увлечение абстракциями также приводит к когнитивной перегрузке.
Простые потоки выполнения
Упрощайте потоки управления (control flows). Функции, содержащие слишком много операторов if или циклов, сложны для понимания и запоминания, поскольку трудно удержать в кратковременной памяти весь сложный поток управления (подробнее об упрощении потоков — здесь). Скрывайте сложную логику в вспомогательных (helper) функциях и сокращайте вложенность (как?), используя так называемые ранние возвраты (early returns) для обработки особых случаев.
Минимизируй изменения состояний
Минимизируйте количество изменяемых состояний (mutable states). Код без изменений состояния (так называемый stateless-код) проще для понимания. Например, по возможности избегайте изменяемых (мутабельных) полей классов, и по возможности делайте типы неизменяемыми (иммутабельными).
Тестируй только релевантное здесь и сейчас
Включайте в тесты только необходимое. Тест будет сложным для понимания, если в него включены какие-то шаблонные (boilerplate) данные, не имеющие прямого отношения к этому тест-кейсу, а релевантные и нужные данные наоборот скрыты где-то в дополнительных функциях.
Моков не должно быть слишком много
Не злоупотребляйте моками (имитаторами) в тестах. Неправильное или чрезмерное их применение может привести к тому, что тесты будут переполнены излишними вызовами с деталями имплементации тестируемой системы.
Еще раз о релевантных деталях в тесте
Рассмотрим этот пункт подробнее и на практическом примере.
Какая, на ваш взгляд, проблема в приведенном ниже коде усложняет понимание (да и выполнение)?
def test_get_balance(self): settings = BankSettings(FDIC_INSURED, REGULATED, US_BASED) account = Account(settings, ID, BALANCE, ADDRESS, NAME, EMAIL, PHONE) self.assertEqual(account.GetBalance(), BALANCE)
Проблема заключается в том, что в этом коде (создания банковского счета) много лишнего (назовём это «шумом»), из-за чего трудно с ходу понять, какие детали здесь относятся к ассерту (утверждению).
Но, заметьте, переход от одной крайности в другую также может создать проблемы:
def test_get_balance(self): account = _create_account() self.assertEqual(account.GetBalance(), BALANCE)
Теперь проблема в том, что критически важные детали скрыты («свёрнуты») в helper-функции _create_account()
, поэтому не очень ясно, что с полем BALANCE
. Для того чтобы сразу и правильно понять этот тест, лучше «переключить контекст», в helper-функцию.
Хороший тест должен включать только релевантные детали, имеющие прямое отношение к тому что делает тест, и скрывать лишнее, «шумы»:
def test_get_balance(): account = _create_account(BALANCE) self.assertEqual(account.GetBalance(), BALANCE)
Если прислушаться к этому совету, и сделать по best practices, то будет легче проследить поток выполнения от начала до конца теста. Например:
Плохой код | Правильный код |
def test_bank_account_overdraw_fails(self): account = _create_account() outcome = _overdraw(account) self._assert_withdraw_failed( outcome, account) def _create_account(): settings = BankSettings(…) return Account(settings, BALANCE, …) def _overdraw(account): # Boilerplate code … return account.Withdraw(BALANCE + 1) def _assert_withdraw_failed( self, outcome, account): self.assertEqual(outcome, FAILED) self.assertEqual( account.GetBalance(), BALANCE) | def test_bank_account_overdraw_fails(self): account = _create_account(BALANCE) outcome = _withdraw(account, BALANCE + 1) self.assertEqual(outcome, FAILED) self.assertEqual( account.GetBalance(), BALANCE) def _create_account(balance): settings = BankSettings(…) return Account(settings, balance, …) def _withdraw(account, amount): # Boilerplate code … return account.Withdraw(amount) |