Настоящая цена технического долга
Reading time: 6 minutes
Last modified:
Спросите комнату инженеров, что такое технический долг, и получите дюжину разных определений. Хакерский код. Отсутствующие тесты. Устаревшие зависимости. «Всё, что написала предыдущая команда». Это слово применяется к стольким разным вещам, что потеряло большую часть практического смысла.
Исходное определение Уорда Каннингема 1992 года было точнее и полезнее: технический долг — это разрыв между дизайном, который вы поставили, и дизайном, который, как вы теперь знаете, был бы лучше. Это цена решений, принятых в условиях ограничений — времени, знаний, ресурсов, — которые нужно пересматривать по мере изменения этих ограничений.
По этому определению, некоторый технический долг неизбежен. Иногда он даже оправдан.
Миф: весь технический долг плох
Есть версия инженерной культуры, где «у нас технический долг» — всегда констатация неудачи, обвинение прошлых решений. Это неверно практически и контрпродуктивно для команды.
Намеренное накопление технического долга — когда вы осознаёте, что делаете именно это, — легитимное инженерное решение. Выпустить работающую фичу с черновой реализацией, чтобы успеть к дедлайну, с намерением разобраться позже, когда фича подтвердит свою ценность, — разумный компромисс. Продукт выходит на рынок, фича тестируется с реальными пользователями, команда выплачивает долг, убедившись, что это того стоит.
Проблемный долг — не тот, что накоплен осознанно. Тот, что накоплен случайно, или тот, что взяли намеренно, а потом забыли вернуть.
Квадрант
Фреймворк Каннингема (позже расширенный Мартином Фаулером) строит долг по двум осям:
| Осмотрительный | Безрассудный | |
|---|---|---|
| Намеренный | «Мы знаем, что срезаем угол — исправим после запуска.» | «Нет времени делать это правильно.» |
| Ненамеренный | «Теперь мы лучше понимаем домен — спроектировали бы иначе.» | «Что такое слоение? Что такое связанность?» |
Верхний левый квадрант — здоровая инженерия. Вы приняли осознанное решение, приняли компромисс и планируете его пересмотреть.
Верхний правый — проблема процесса. Команды под хроническим давлением накапливают безрассудный намеренный долг: они знают, что делают неправильно, но нет пространства делать правильно.
Нижний левый — признак обучения: вы понимаете домен лучше, чем когда строили. Именно так работает разработка ПО.
Нижний правый — по-настоящему опасный вид: технический долг, о котором никто не знает, потому что никто не понимал, как выглядит хорошее решение, с самого начала. Его сложнее всего исправить, потому что те, кто должен исправлять, не знают, что нужно исправлять.
Как реально измерять долг
Процент покрытия тестами и строки кода — вводящие в заблуждение метрики. Кодовая база может иметь 80% покрытия тестами и при этом быть в ужасном состоянии. Кодовая база без тестов может быть чистой и хорошо структурированной.
Метрика, которая реально отражает технический долг, — время до фичи: сколько времени занимает добавление новой возможности в систему. А именно:
- Сколько времени нужно опытному разработчику, чтобы понять нужную часть кодовой базы, прежде чем он сможет что-то изменить?
- Сколько несвязанных файлов приходится менять при модификации одного компонента?
- Как долго типичный PR висит на ревью, потому что ревьюеры не понимают контекста?
- Как часто исправление одного бага вносит новый?
Если эти цифры растут со временем, пока команда набирается опыта и система развивается, — это отпечаток накапливающегося долга. Если они стабильны или снижаются, команда управляет им нормально, независимо от того, как код выглядит эстетически.
Эффект накопления
Финансовая метафора долга уместна, потому что долг накапливается сложным процентом. Кодовая база с умеренной связанностью в одной области притягивает больше связанности. Инженеры, работающие в замусоренной области, пишут замусоренный код — частично потому что плохой код сложнее рефакторить постепенно, частично потому что «здесь и так беспорядок» — реальный психологический сигнал, снижающий аккуратность.
Накопление проявляется конкретными способами:
Скорость разработки фич падает. Что занимало один спринт в первый год, занимает три спринта в третий. Команда не стала медленнее — кодовая база накопила трение.
Баги кластеризуются. Когда одни и те же три файла постоянно фигурируют в постмортемах, долг локализован. Это полезная информация: она показывает именно, где сосредоточить работу по исправлению.
Время онбординга растёт. Новый инженер, которому нужно четыре недели, чтобы сделать первый значимый вклад, сигнализирует о проблеме передачи знаний, которая обычно коренится в случайной сложности — вещи трудно понять, хотя не должны быть.
Изменения ощущаются рискованными. Когда опытные инженеры колеблются перед тем, как тронуть какую-то область кодовой базы, это колебание что-то измеряет реальное. Хорошо понятые и структурированные системы ощущаются безопасными для изменений. Системы с долгом — опасными.
Когда выплачивать
Выплачивайте технический долг, когда перечисленные выше сигналы появляются в совокупности:
- Скорость разработки фич в конкретной области измеримо снизилась по сравнению с тем, что было
- Баги кластеризуются в одних и тех же компонентах
- Онбординг в этот компонент занимает непропорционально много времени
- Инженеры неохотно прикасаются к нему
Эти сигналы несут бизнес-стоимость, которую можно оценить. Если фича сейчас занимает три спринта, а в рефакторированной системе заняла бы один, долг обходится в два спринта на каждую фичу. Это конкретное число для разговора о необходимости исправления.
Сильнейший аргумент в пользу выплаты долга — когда затронутый код находится на критическом пути предстоящих работ. Рефакторите модуль, который собираетесь расширять, а не тот, к которому два года никто не прикасался.
Когда НЕ выплачивать
Когда код не на критическом пути. Код, который работает, стабилен и никогда не будет меняться, не должен быть красивым. Трогать его — создавать риск без соответствующей пользы. Оставьте в покое.
Когда рефакторинг означает переписывание. Полные переписывания почти всегда создают новый долг, выплачивая старый. Существующая система, как бы беспорядочна она ни была, кодирует огромное количество накопленного поведения, обработки граничных случаев и неявных продуктовых знаний. Переписывание начинается с нуля и обычно заново открывает те же проблемы. Кладбище провалившихся глобальных переписываний обширно.
Когда нет достаточного понимания проблемы. Преждевременный рефакторинг — чистка кода в домене, который вы не вполне понимаете, — часто делает хуже. Подождите, пока не поработаете в области достаточно, чтобы знать, какие абстракции реально правильны.
Когда бизнес меняется быстрее технологий. При быстрых итерациях над продуктом выплата архитектурного долга до стабилизации формы продукта часто оказывается потраченной впустую работой. Подождите, пока продукт устоится, прежде чем укреплять архитектуру.
Рефакторинг в процессе vs выделенные спринты
Оба работают, и ни один не работает без другого.
Рефакторинг в процессе — правило бой-скаута: оставляй код чуть лучше, чем нашёл. Работая в какой-то области, устраняйте локальные проблемы — переименуйте запутанные переменные, выделите функцию, добавьте тест для только что изменённого пути. Это предотвращает накопление без необходимости выделять специальное время.
Выделенные спринты нужны для более крупных структурных проблем — проблем на уровне архитектуры, которые нельзя исправить мимоходом. Модуль с неправильной границей абстракции, схема базы данных, требующая миграции, API-контракт с накопившимися обратно совместимыми хаками. Всё это требует сосредоточенных усилий и несёт риск, который должен быть явно запланирован.
Ошибка — рассматривать их как альтернативы. Команды, полагающиеся только на рефакторинг в процессе, никогда не решают структурные проблемы. Команды, полагающиеся только на выделенные спринты, никогда не получают от бизнеса одобрения на время, и мелкие проблемы накапливаются в промежутках.
Как говорить об этом с нетехническими стейкхолдерами
«Нам нужно выплатить технический долг» плохо воспринимается людьми, которые не пишут код. Звучит как инженеры, желающие убраться в своём беспорядке вместо того, чтобы строить фичи.
Работающий перевод: стоимость изменения. «Прямо сейчас добавление фичи в модуль биллинга занимает три недели из-за его структуры. Если мы потратим две недели на реструктуризацию, следующие пять фич будут занимать по одной неделе вместо трёх. Это чистая экономия восьми недель на следующих пяти фичах.»
Обычно этот аргумент точен. Задача — измерить текущее трение и оценить улучшение. Если не получается обосновать цифрами, скорее всего, работа не оправдана.
Получили в наследство кодовую базу и не знаете, с чего начать? Напишите нам на hello@cimpleo.com. Проаудируем кодовую базу и скажем, что стоит исправить, а что лучше оставить как есть.