Хороший код: как понять, что его будет удобно менять
Понятные имена, небольшие методы и аккуратное форматирование делают код проще для чтения. Но даже аккуратный файл может оказаться дорогим в работе, если небольшое изменение требует восстановить много контекста и затронуть несколько частей системы.
Вместе с Ринатом, iOS-разработчиком в Naumen, разбираемся, почему хороший код проверяется следующей задачей, как проявляется сложность изменений и на что стоит смотреть при оценке кода.

Код проверяется следующей задачей
Пока работа над задачей еще свежая, почти любой код кажется понятным. Автор помнит, почему вызовы стоят именно в таком порядке, какой случай обсуждали на ревью и что здесь собирались переделать позже. Коллеги тоже держат часть контекста в голове.
Так что даже не самое удачное решение какое‑то время не доставляет особых проблем.
Через полгода ситуация меняется: исходная задача давно закрыта, участники обсуждения заняты другими частями проекта, а другому разработчику нужно внести небольшое изменение.
Именно в этот момент становится понятно, насколько код вообще рассчитан на изменения.
Простая задача может оказаться дорогой
Одна из задач у нас на планировании звучала безобидно:
После ошибки авторизации запрос повторять не нужно, после временной сетевой ошибки — нужно, причем с увеличивающейся задержкой.
Само условие укладывается в несколько строк. Но сначала приходится выяснить, где на самом деле живет это правило: в сетевом клиенте, в сервисе авторизации, в middleware.
А еще нужно понять, не запускает ли часть повторов таймер внутри другого объекта и не зависит ли соседний сценарий от текущего порядка вызовов. В итоге сам код меняется быстро, но день уходит на восстановление картины вокруг него.
Смотреть нужно на изменение, а не на файл
У кода есть свойства, которые легко заметить сразу: понятные имена, небольшие методы, аккуратное форматирование и простая структура. Все это полезно, но само по себе еще не говорит, насколько удобно систему менять.
Можно открыть класс и довольно быстро разобраться в каждом его методе. А потом выяснить, что для добавления одного состояния нужно исправить еще четыре модуля, обновить несколько почти одинаковых преобразований данных и соблюсти порядок вызовов, который нигде явно не зафиксирован.
Файл может выглядеть вполне нормально, а изменение при этом оказывается дорогим.
Мне в этом контексте близко описание сложности изменений у Джона Оустерхаута. Он выделяет три характерных проявления.
-
Маленькая правка расползается по системе
Добавили поле в модель и приходится менять сетевой слой, хранилище, аналитику, несколько экранов и тестовые данные.
Иногда это естественная цена изменения контракта, а иногда — признак того, что одно знание размазано по проекту.
-
Для локальной работы нужно слишком много контекста
Чтобы поправить один обработчик, нужно знать устройство навигации, жизненный цикл экрана, особенности кэша и два исторических обхода старых ошибок.
-
Есть зависимости, о которых разработчик даже не знает
Они обнаруживаются уже после изменения. Например, перестановка двух вызовов отключает сетевую проверку: первый метод использует закэшированное состояние и завершает сценарий раньше времени.
Эти признаки полезнее многих разговоров о «чистом коде»: о длине метода можно спорить, а вот с последствиями изменения — сложнее.
Обычно я смотрю на три вещи
-
Сколько мест потребуется затронуть?
-
Сколько информации нужно восстановить перед работой?
-
Как быстро мы узнаем, что ошиблись?
Чем меньше ответ зависит от памяти конкретного человека, тем спокойнее живется проекту.