Алексей Меленчук
вся записка
дата
объём 6 мин чтения

Технический долг: когда его отдавать, а когда нет

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

Коротко. Платить стоит по тому долгу, по которому идут проценты, — то есть в коде, который меняют. Старый код, который никто не трогает, это не долг, а просто старый код.

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

Одним словом здесь называют две вещи с разной ценой возврата. Пока они не разведены, разговор повторяется на каждом планировании.

Что на самом деле сказал Каннингем

Метафора появилась в отчёте Уорда Каннингема об опыте разработки системы WyCash, представленном на OOPSLA’92. Слов «технический долг» там, кстати, нет — есть просто долг, термином это стало позже. Формулировка короче, чем принято думать: отгрузить код с первого захода — всё равно что взять в долг, и небольшой долг ускоряет разработку, пока его быстро возвращают переписыванием (текст отчёта).

Разрешения писать плохо в этой фразе нет. Сам Каннингем в 2009 году от такой трактовки отдельно отмежевался в разговоре, известном как «Debt Metaphor»: долг у него — расхождение между кодом и сегодняшним пониманием предметной области, а не сэкономленное на аккуратности время. На старте понимание неполное всегда, и долг берётся из решения отгрузить работающую версию раньше, чем область разобрана до конца. Проценты — время, которое команда потом тратит, имея дело с кодом, написанным под устаревшее понимание.

И деталь, которую цитируют реже всего. Масштаб возврата Каннингем прямо не задаёт, но говорит про «unconsolidated implementation» — про консолидацию уже отгруженного куска, а не про новую систему с нуля.

Четыре клетки Фаулера

Мартин Фаулер разложил это на четыре клетки: долг берут намеренно или не замечая, и в обоих случаях — расчётливо или безрассудно (Technical Debt Quadrant).

Намеренно и расчётливо — «выкатываем к дате, знаем, что чиним после». Это заём: известны сумма, срок и место.

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

Незаметно и безрассудно — месиво, написанное без понимания того, как устроено хорошее проектирование, и проценты по нему самые дорогие.

Незаметно и расчётливо — «теперь мы понимаем, как надо было». Этой клетки не избегают и сильные команды: Фаулер пишет, что понимание верного подхода нередко приходит через год работы над проектом — то есть заведомо позже, чем принято решение.

Практическая разница не в морали. Осознанный долг знает своё место: есть задача, комментарий, договорённость, кто-то живой, кто помнит зачем. Случайный размазан по коду и обнаруживается в тот день, когда простая правка почему-то занимает три дня.

Признак: платить или жить дальше

Долг отдают там, где по нему идут проценты.

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

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

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

Почему «перепишем с нуля» дороже, чем кажется

У Джоэла Спольски есть текст 2000 года, который с тех пор только подтверждался: он называл это худшей стратегической ошибкой софтверной компании и разбирал на примере Netscape (Things You Should Never Do).

Аргумент не про сроки — про то, что теряется. Странное ветвление посреди функции, выглядящее мусором, вполне может оказаться исправлением, найденным в бою: редкое состояние, отказ диска, поведение конкретной версии ядра, чей-то давно забытый прокси. Этого знания нет ни в чьей голове и нет в документации, оно есть только в коде. Беда в том, что по виду ветвления вы не отличите такое исправление от настоящего мусора, — и переписывание выбрасывает и то и другое, чтобы через год собрать заново теми же неделями отладки.

Вторая половина счёта: пока идёт переписывание, старая система живёт и меняется. Новая догоняет движущуюся цель, а команда обслуживает обе.

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

Что делают вместо

Обрезают по кускам. Новая реализация ставится рядом, вызовы переводятся частями, старое удаляется, когда на него никто не ссылается. Условие, без которого это не работает: между старым и новым должна быть граница, выраженная в правиле сборки, а не в договорённости. Договорённость живёт до первого срочного релиза.

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

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

Условие, при котором я неправ

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

И второе. Разговор про проценты предполагает, что команда способна оценить, сколько времени отнимает код. Там, где состав сменился целиком, а истории решений не осталось, дорогим выглядит вообще всё — просто потому что незнакомое. Сначала нужны месяцы работы руками, и только потом список долгов. Иначе первым в него попадёт самое непонятное. Непонятное дешевеет от недели чтения кода; дорогое — нет.