Перейти к основному содержимому

Как маленькие решения создают большой технический долг

«Мы исправим это позже».

Я говорил это. Вы, вероятно, тоже. Каждый разработчик, работающий под дедлайном, говорил это хотя бы раз, обычно отправляя коммит, которым не совсем гордится. Это становится своего рода коллективной мантрой в занятых инженерных командах — способ признать, что что-то не так, и дать себе разрешение двигаться дальше.

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

Технический долг обычно начинается не с катастрофического архитектурного решения. Он начинается с чего-то гораздо более приземлённого: жёстко заданного значения, функции, которая делает четыре дела вместо одного, переменной с именем temp2, потому что temp уже занято. Каждое решение незначительно. Совокупность — нет.

Что такое технический долг?

Термин принадлежит Уорду Каннингему, который использовал аналогию финансового долга для описания ситуации, которую большинство разработчиков уже интуитивно узнавали. Когда вы срезаете путь, чтобы сдать продукт быстрее, вы занимаете в счёт будущего времени разработки. Как и в случае с реальным долгом, он накапливает проценты: чем дольше вы его оставляете, тем дороже его отдавать.

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

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

Долг, который причиняет реальный ущерб, обычно второго типа.

Маленькие решения, которые медленно становятся большими проблемами

Давайте конкретизируем. Вот такие решения кажутся вполне разумными в момент принятия и становятся болезненными год спустя.

Пропуск код-ревью

PR, который «очевидно работает», сливается без надлежащего ревью. Один раз. Затем становится нормой сливать свои маленькие исправления без ожидания. Затем культура ревью начинает размываться в команде. Качество кода постепенно ухудшается, и никто не может указать, когда это началось.

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

// «Временное» исправление, добавленное в спешке, никогда не пересмотрено
function getUser(id) {
  // TODO: удалить эту жёстко заданную перезагрузку, когда починят аутентификацию
  if (id === 99) return { id: 99, name: "Admin", role: "superuser" };
  return db.users.findById(id);
}

Такой код коммитится с полным намерением его почистить. Затем аутентификация чинится, TODO остаётся, и шесть месяцев спустя новый разработчик понятия не имеет, почему существует особый случай для пользователя 99 и безопасно ли его удалять.

Лучший подход, даже в условиях дедлайна:

function getUser(id) {
  return db.users.findById(id);
}

// Temporary: emergency admin override, tracked in issue #482
// Safe to remove once auth middleware is fixed
const EMERGENCY_ADMIN_ID = 99;

Логика та же. Но вторая версия документирует намерение, ссылается на задачу и упрощает очистку для кого-то другого, даже если этим кем-то будете вы в будущем.

Плохие имена переменных и функций

def process(d, f, x):
    return d * (1 + f) ** x

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

Хорошие имена ничего не стоят в момент написания и приносят дивиденды на протяжении всей жизни кодовой базы.

Копипаста

Дублирование логики между файлами — один из самых быстрых способов создать кошмар поддержки. Когда нужно изменить, как что-то работает, вы должны найти каждую копию. Одну вы пропустите. Баг, исправленный в трёх местах, останется в четвёртом и проявится в проде в самый неподходящий день.

Огромные функции

Функции, которые делают слишком много, сложно тестировать, сложно понимать и сложно безопасно модифицировать. Я наследовал функции длиной в двести строк, которые касались всего — от запросов к базе до форматирования писем. Рефакторинг их был отдельным проектом — тем, на который ни у кого никогда не было времени, что и объясняет, как они стали такими длинными.

Игнорирование тестов

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

Реальный опыт проекта

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

Но когда я начал копаться в кодовой базе, долг был повсюду. Функции, написанные наспех и никогда не пересматривавшиеся, обрабатывающие граничные случаи с помощью стека условных операторов, накопленных со временем. Запросы к базе, написанные прямо в компонентах без слоя абстракции, так что любое изменение схемы превращалось в поиск и замену в десятках файлов. Значения конфигурации, жёстко заданные как минимум в шести разных местах, из-за чего каждое обновление окружения требовало изменения кода, а не конфигура.

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

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

Стоимость очистки была оценена более чем вдвое больше, чем стоило бы избежать долга изначально.

Скрытая стоимость технического долга

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

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

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

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

Онбординг занимает больше времени, потому что новые разработчики не могут полагаться на самодокументированность кода. Им нужен кто-то, кто объяснит, почему всё так, как есть, — а значит, нужен старший разработчик, у которого, вероятно, есть другие дела.

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

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

Как я пытаюсь этого избежать

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

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

Относись к код-ревью серьёзно. Ревью должно быть для понимания, а не только для одобрения. Спросить «почему это работает?» часто ценнее, чем «работает ли это?». Хорошее ревью вылавливает не только баги, но и сложность, вносимую без необходимости.

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

Пиши осмысленные имена. Время, потраченное на хорошее именование, окупается каждый раз, когда кто-то читает код. А код читают гораздо чаще, чем пишут.

Документируй намерение, а не механику. Комментарии, объясняющие, что делает код, — в основном шум: код уже это показывает. Комментарии, объясняющие, почему было принято решение, особенно неочевидное, действительно ценны.

Пиши тесты. Знаю. Все знают. И всё же. Тесты — лучшая инвестиция в долгосрочную поддерживаемость, и они делают рефакторинг значительно менее пугающим.

Удаляй неиспользуемый код. Мёртвый код — это шум. Он затрудняет навигацию по кодовой базе и создаёт ложное представление о том, что делает система. Если код не используется, удаляйте его — система контроля версий сохраняет историю, если он вдруг понадобится.

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

Извлечённые уроки

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

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

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

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

Ключевые выводы

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

«Мы исправим это позже» — самый распространённый первый шаг к долгу, и «позже» последовательно не наступает.

Стоимость технического долга — не только во времени, но и в моральном духе команды, трениях при онбординге, частоте багов и скорости поставки.

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

Сделать долг видимым (через комментарии, задачи, честную документацию) — первый шаг к управлению им.

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

Заключение

Технический долг создаётся не одним плохим решением. Он создаётся сотнями маленьких решений, которые никто не оспаривал.

Цель не в том, чтобы полностью его устранить — некоторый долг является ценой движения в реальном мире при реальных ограничениях. Цель — быть осознанным, когда вы его берёте, честным в отношении его цены и последовательным в его погашении, прежде чем проценты станут неподъёмными.

Лучшее время для решения технического долга — до того, как он станет причиной медленной работы команды. Второе лучшее время — сейчас.

Источникdevto