Поддержка, технический долг и эволюция продукта
В предыдущей статье мы довели изменение до продакшена: оно задеплоено, метрики зелёные, дежурный не разбужен. В учебниках здесь обычно ставят точку и рисуют стрелку обратно к «требованиям». В жизни именно с этого момента начинается самая длинная фаза жизни системы — та, в которой она проведёт следующие пять, десять или двадцать лет.
Это не преувеличение. Оценки доли сопровождения в полной стоимости владения ПО гуляют в диапазоне 60–90% — впервые их системно собрали Лиенц и Свонсон в конце 1970-х, потом подтверждали Боэм, Гласс и другие. Числа спорные и сильно зависят от того, что считать сопровождением, но порядок величины устойчив: писать код с нуля — меньшая часть работы индустрии. Большая часть — менять код, который уже работает и который написал кто-то другой (иногда вы полгода назад).
Джуниор обычно приходит с противоположной картиной мира: интересное — это новые фичи, а поддержка — наказание, куда ссылают неудачников. Эта статья пытается заменить картинку на честную: разобрать, как устроен поток обращений, что такое технический долг в терминах, которые работают в разговоре с бизнесом, как системы стареют и почему умение жить с чужим кодом — один из главных водоразделов между грейдами.
Сопровождение — это четыре разных занятия
Слово «поддержка» склеивает несколько видов работы, у которых разные заказчики, разные сроки и разная ценность. Классификация из ISO/IEC 14764 (и восходящая к тем же Лиенцу и Свонсону) делит сопровождение на четыре типа.
| Тип | Что это | Пример | Кто инициирует |
|---|---|---|---|
| Корректирующее | чиним то, что сломано | баг в расчёте скидки | пользователь, мониторинг, тестировщик |
| Адаптивное | подстраиваемся под изменившееся окружение | банк поменял формат выписки; вышел новый iOS; изменился закон | внешний мир |
| Совершенствующее | улучшаем то, что работает | ускорить отчёт с 40 до 4 секунд, добавить фильтр | пользователи, продакт |
| Превентивное | делаем будущие изменения дешевле | обновить зависимости, вынести дублирующуюся логику, добавить тесты | команда |
Практический смысл этого деления не академический. Когда бизнес говорит «мы слишком много тратим на поддержку», почти всегда имеется в виду первый тип — исправление багов. Но в реальных замерах корректирующее сопровождение — это лишь около пятой части, а половина и больше приходится на совершенствующее, то есть на развитие продукта, просто называющееся другим словом. Умение показать это разделение — уже полезный навык: «мы не чиним поломки, мы 60% времени строим новое в старой системе» звучит совсем иначе, чем «поддержка съедает команду».
Превентивное сопровождение — это и есть выплата технического долга, и это единственная категория, у которой нет внешнего заказчика. Никто извне никогда не попросит обновить зависимости. Именно поэтому она первой вылетает из плана.
Как устроен поток обращений: линии поддержки
Между пользователем, у которого «ничего не работает», и разработчиком, который может починить, обычно стоит несколько фильтров. В классической модели (она пришла из ITIL и service desk-практик) их называют линиями.
- L1 — первая линия. Операторы поддержки, чат, телефон, тикет-система. Не читают код. Задача — понять, что случилось, применить известное решение из базы знаний, отсеять «забыл пароль» и «не оплачен тариф».
- L2 — вторая линия. Технически грамотные люди: инженеры поддержки, иногда выделенные аналитики. Умеют смотреть логи, дёргать API, воспроизводить, знают типовые сбои. Отсеивают всё, что не требует изменения кода.
- L3 — третья линия. Команда разработки. Сюда доезжает то, что требует правки кода, миграции данных или архитектурного решения.
уточнение почты и времени L1->>L2: эскалация: воспроизводится, не единичный L2->>L2: логи, traceId, статус письма в провайдере alt Известная причина, кода не касается L2-->>U: ответ + обходной путь else Нужна правка кода L2->>D: тикет: шаги, traceId, версия, масштаб D->>D: триаж, воспроизведение, оценка D->>D: фикс + тест на регресс D-->>L2: релиз 4.18.2, что проверить L2-->>U: подтверждение, что починено end M-->>D: параллельно: алерт по росту ошибок SMTP Note over M,D: Мониторинг часто узнаёт
о проблеме раньше пользователя
Ключевая деталь, которую стоит понять сразу: линии существуют не чтобы отгородить разработчиков от людей, а чтобы защитить дорогое и редкое время от того, что решается дешевле. Час разработчика стоит компании кратно дороже часа оператора L1, и если разработчик каждый день по десять раз объясняет, где в интерфейсе кнопка «скачать акт», компания платит консультанту зарплату сеньора.
Обратная сторона — глухота. Когда между командой и пользователем три фильтра, до разработчика доезжает не проблема, а её пересказ через три головы. Поэтому хорошие команды пробивают в этой стене окна: разработчик дежурит в общем канале поддержки раз в N недель, читает сырые обращения, слушает записи звонков. Это разово неэффективно и системно окупается.
Энтерпрайз и стартап: две разные реальности
| Крупная компания | Стартап | |
|---|---|---|
| Линии | формальные L1/L2/L3, отдельные подразделения, регламент эскалации | линий нет: разработчик сам сидит в чате поддержки |
| Инструмент | ServiceDesk/Jira Service Management, SLA в договоре | Intercom, Telegram-чат, иногда личный телефон CTO |
| Скорость фикса | недели: тикет, приоритизация, релизное окно, регресс | часы: увидел — починил — выкатил |
| Что видит джун | отфильтрованную формулировку задачи | живого недовольного человека |
| Риск | проблема живёт долго, все привыкли | чинят громких, а не важных; выгорание от постоянных прерываний |
Ни одна из моделей не «правильнее». В банке цена ошибки в проде такова, что фильтры и регламент экономят больше, чем стоят. В стартапе на двадцать клиентов регламент убил бы скорость, а прямой контакт с пользователем — источник продуктовых инсайтов, за которые энтерпрайз платит исследовательским агентствам. Проблемы начинаются, когда модель не соответствует размеру: стартап на 300 человек, где сеньоры всё ещё сами отвечают в чате, и корпорация, где фикс опечатки в тексте кнопки требует четырёх согласований.
Триаж: как решается, что чинить первым
Триаж (от медицинской сортировки раненых) — процедура, в которой входящий поток разбирается на «чиним сейчас», «чиним в плане», «не чиним». Ошибка джуна — считать это бюрократией. На деле это единственная защита от состояния, когда всё горит и никто ничего не делает.
traceId, время, скриншот] C --> C1{Данные пришли?} C1 -- нет за N дней --> Z1[Закрыть: Cannot reproduce] C1 -- да --> B B -- да --> D{Это баг
или ожидаемое поведение?} D -- ожидаемое --> Z2[Not a bug →
в бэклог продукта как идея] D -- баг --> E{Затронуты деньги,
данные или безопасность?} E -- да --> S1[S1: чиним сейчас,
будим дежурного] E -- нет --> F{Есть обходной путь?} F -- нет --> S2[S2: в ближайший релиз] F -- да --> G{Сколько людей задето
и как часто?} G -- много/часто --> S2 G -- мало/редко --> H{Стоимость фикса
vs ущерб} H -- фикс дешевле --> S3[S3: в бэклог с приоритетом] H -- фикс дороже --> Z3[Won't fix:
решение записать явно]
Severity и priority — это разные шкалы
Их путают постоянно, и путаница дорого стоит.
- Severity (критичность) — объективное свойство дефекта: насколько сильно ломается система. Оценивает тот, кто нашёл: тестировщик, инженер поддержки.
- Priority (приоритет) — решение бизнеса: когда мы это чиним. Оценивает продакт или владелец продукта.
Они не совпадают, и это нормально. Приложение падает при открытии экрана, которым пользуются два клиента в год, — severity высокая, priority низкая. Опечатка в названии компании на главной странице — severity минимальная, priority максимальная, потому что завтра презентация инвесторам.
Типичная шкала severity:
| Уровень | Смысл | Реакция |
|---|---|---|
| S1 / Blocker | сервис недоступен, теряются деньги или данные, утечка | немедленно, вне очереди, поднимают людей |
| S2 / Critical | ключевой сценарий не работает, обходного пути нет | в ближайший релиз или хотфиксом |
| S3 / Major | функция работает неверно, есть обходной путь | планово, в спринт |
| S4 / Minor | косметика, редкий сценарий | когда-нибудь / никогда, честно |
Что должно быть в баг-репорте
Джуну важно уметь читать баг-репорты и писать их — вы будете и получателем, и отправителем. Минимальный набор, без которого тикет вернётся:
Заголовок: [Биллинг] Двойное списание при повторной отправке формы оплаты
Окружение: prod, версия API 4.18.1, веб, Chrome 138, пользователь id=88421
Время: 2026-07-14 11:42–11:44 MSK
traceId: 7f3c9a1e-...
Шаги:
1. Открыть /billing/upgrade, выбрать тариф Pro
2. Нажать «Оплатить», в течение 3 секунд нажать ещё раз
3. Дождаться редиректа
Ожидается: одна транзакция
Фактически: две транзакции, обе успешны, баланс списан дважды
Масштаб: 14 обращений за неделю, ~0.3% попыток оплаты
Обходной путь: возврат вручную через админку
Логи/скриншоты: приложены
Три поля, которые джуны забывают чаще всего и которые решают половину дела: traceId (позволяет найти запрос в логах за секунды вместо часа), масштаб (без него невозможно приоритизировать) и обходной путь (определяет, S2 это или S3).
Жизненный цикл бага
У задачи-фичи путь в трекере простой. У бага он ветвистее, потому что у него есть исходы, которых у фичи не бывает: «не воспроизводится», «не баг», «дубликат», «не будем чинить».
Несколько практических замечаний к этой схеме.
«Won’t fix» — честный и нужный статус. Команды, которые стесняются его ставить, накапливают бэклог из тысячи багов, который никто никогда не читает, и в котором тонут настоящие проблемы. Тикет, висящий три года со статусом «открыт», врёт сильнее, чем закрытый с формулировкой «редкий сценарий, стоимость фикса — неделя, ущерб — одно обращение в год, решение пересмотрим при росте обращений».
Переоткрытие — сигнал, а не позор. Высокий процент reopened означает одно из двух: чинят симптом вместо причины либо нет теста, фиксирующего исправление. Отсюда железное правило: каждый исправленный баг в проде получает автотест, воспроизводящий его. Не «хорошо бы», а часть Definition of Done — см. Разработка.
Пятикратное «почему». Если баг S1 — недостаточно починить код. Нужно понять, почему он доехал до прода: не было теста? не покрыли кейс в требованиях? не сработал алерт? Разбор инцидентов и постмортемы подробно разобраны в Релизе и эксплуатации; здесь важно другое — follow-up actions из постмортемов и есть самый качественный, осмысленный техдолг. Он уже обоснован реальной болью, и его проще всего защитить перед бизнесом.
Бюджет поддержки: как команда не тонет
Дальше начинается арифметика, которую джун обычно не видит, но от которой зависит его рабочая неделя.
Поток багов и обращений не останавливается. Если команда планирует спринт под завязку фичами, то каждый входящий баг вышибает из плана что-то запланированное. Через три спринта продакт перестаёт верить оценкам, команда живёт в режиме постоянного прерывания, и никто не понимает, куда девается время.
Рабочие практики:
- Резерв ёмкости. 10–30% спринта закладывается под непредвиденное. Конкретный процент вычисляется из истории: посмотрите, сколько незапланированной работы реально было в последние 5–10 спринтов. Если по факту уходит 40%, а планируете вы 0% — проблема не в людях.
- Дежурный по поддержке. Один человек в спринте (ротацией) снимает весь входящий поток: разбирает тикеты, отвечает L2, чинит мелочь. Остальные защищены от прерываний. У джуна это лучшая неделя месяца с точки зрения обучения — вы за пять дней увидите больше кусков системы, чем за месяц работы над одной фичей.
- Отдельный класс обслуживания. В канбане это expedite-дорожка с жёстким лимитом: одновременно не больше одного «срочного». Механика лимитов и классов обслуживания разобрана в Kanban и поток.
- Правило «двух касаний». Если один и тот же вопрос от поддержки пришёл дважды — это не тикет, это дефект продукта или дыра в документации. Чините источник.
Типичная ошибка — считать поддержку «фоном», который делается «между делом». Прерывание стоит не пятнадцать минут, а пятнадцать минут плюс возврат в контекст; исследования по многозадачности разработчиков дают оценки восстановления контекста в десятки минут. Три «быстрых вопроса» в день — это минус половина продуктивного дня.
Технический долг: определение, которое работает
Теперь к главной теме. Термин придумал Уорд Каннингем в отчёте на OOPSLA'92 (оригинальный текст), и в народной интерпретации его смысл потеряли почти полностью.
Каннингем говорил не о плохом коде. Он говорил вот о чём: вы выпускаете код, отражающий ваше сегодняшнее, неполное понимание задачи. Это нормально и даже правильно — иначе вы никогда ничего не выпустите. Но с этого момента вы платите проценты: пока код не приведён в соответствие с выросшим пониманием, каждое изменение обходится дороже. Сам Каннингем позже уточнял это в видео о метафоре долга: «долг — это не разрешение писать плохой код».
Отсюда следует важное практическое разграничение:
- Это долг: год назад мы сделали одну таблицу
usersпод одну страну; теперь стран пять, у каждой свои требования к персональным данным, и модель им не соответствует. Мы понимаем, как должно быть, но код этого ещё не отражает. - Это не долг, это брак: разработчик не знал про SQL-инъекции и склеил запрос строками. Это дефект, чинится как дефект, а не планируется как «долг».
- Это не долг, это вкусовщина: код написан в другом стиле, чем вы бы написали. Если он покрыт тестами и понятен — это просто чужой код.
Смешивание этих трёх категорий — причина, по которой фраза «у нас много техдолга» ничего не значит для бизнеса. В ней спрятаны и реальный риск, и незнание, и обида на предшественника.
Квадрант Фаулера
Мартин Фаулер предложил классификацию по двум осям: осознанность и безрассудность.
Практический вывод: только правый верхний квадрант — управляемая инженерная стратегия. Она допустима, если долг записан (тикет, комментарий, ADR) и есть условие его возврата («до 500 клиентов живём так, дальше переделываем»). Всё остальное — либо проблема с квалификацией (лечится обучением и ревью), либо проблема с процессом (лечится не рефакторингом, а разговором с руководством).
Нижний правый квадрант — самый частый и самый недооценённый: вы честно спроектировали как умели, а через год домен раскрылся иначе. Это не чья-то вина, это природа разработки. Ровно об этом писал Каннингем.
Как проявляются проценты
Проценты платятся не деньгами со счёта, а незаметно — временем, которое уходит непонятно куда:
- одна и та же по размеру фича делается уже не три дня, а полторы недели, и никто не может внятно объяснить почему;
- перед релизом команда нервничает, тестируют «на всякий случай» всё подряд;
- задачу может взять только один человек, потому что «только Петя знает, как там устроено» (bus factor = 1);
- CI идёт 40 минут, из них 15 — перезапуски флаки-тестов;
- новый человек выходит на самостоятельную работу не за две недели, а за два месяца;
- на любой вопрос «а что будет, если…» ответ «надо проверить», а не «вот так».
Обратите внимание: ни один из симптомов не звучит как «код некрасивый». Все они измеримы и все переводятся в деньги. Это и есть язык, на котором о долге разговаривают с менеджментом.
Виды долга: у каждого свои проценты
«Технический долг» — зонтик над очень разными вещами, у которых сильно разная скорость нарастания и разная цена возврата.
| Вид | Пример | Как нарастает | Цена возврата |
|---|---|---|---|
| Кодовый | дублирование, гигантские функции, магические числа | линейно, медленно | низкая, локальная |
| Тестовый | нет тестов, флаки, тесты на реализацию | быстро: без тестов страшно менять всё остальное | средняя, но окупается сразу |
| Архитектурный | сервис знает про таблицы другого сервиса | медленно, но необратимо: поверх строят новое | очень высокая |
| Данных | схема не отражает домен, денормализация «на время» | быстро: данные накапливаются, миграция дорожает каждый день | высокая, требует миграций |
| Зависимостей | Java 8, библиотека без поддержки с 2019 | сам по себе, пока вы спите | растёт скачками (EOL, CVE) |
| Инфраструктурный | деплой руками по инструкции в вики | линейно + риск человеческой ошибки | средняя |
| Документационный | документация врёт | быстро обесценивается | низкая, если делать регулярно |
| Знаниевый | ушёл единственный человек, знавший модуль | скачком при увольнении | самая высокая: восстанавливать нечего |
Отдельно про долг зависимостей, потому что его недооценивают чаще всего. Это единственный вид долга, который растёт, даже если вы не написали ни строчки кода. Мир движется: библиотека выходит из поддержки, находят CVE, облачный провайдер объявляет EOL версии managed-базы, магазин приложений требует новый target SDK. В какой-то момент «просто обновиться» становится проектом на квартал, потому что между вашей версией и актуальной — четыре мажорных релиза с ломающими изменениями.
Практика, которая дешевле любого героизма: обновляться регулярно и понемногу, автоматически (Dependabot, Renovate), с политикой «обновление зависимостей — обычный PR, а не проект». Плюс инвентаризация: что у нас стоит, до какой даты поддерживается, что уже в статусе EOL. Пересечение с безопасностью здесь прямое — см. Безопасность в пайплайне.
Как измерять долг, не обманывая себя
Первый порыв — прикрутить статический анализатор, посмотреть на «technical debt: 342 days» в SonarQube и предъявить это менеджменту. Не работает: число получено из эвристик про стиль кода и не отвечает на вопрос «сколько нам это стоит».
Измерять стоит последствия, а не свойства кода:
- Доля незапланированной работы. Сколько процентов ёмкости за квартал ушло на баги, инциденты и «срочно посмотреть». Растёт — долг растёт.
- Lead time для изменения. Сколько проходит от коммита до прода; и отдельно — сколько занимает типовая фича сейчас против года назад. Метрики DORA и как их правильно читать — в DORA и инженерных метриках.
- Change failure rate. Доля релизов, потребовавших отката или хотфикса.
- Время прогона CI и доля флаки-тестов. Прямая, ежедневная, всеми ощущаемая цена.
- Возраст зависимостей. Медианное отставание от актуальных мажорных версий; количество компонентов вне поддержки.
- Время онбординга. Сколько недель до первого самостоятельного изменения в проде.
А чтобы понять, куда именно вкладывать деньги, полезнее всего пересечь две вещи: насколько файл сложный и насколько часто его трогают. Идея принадлежит Адаму Торнхиллу («Your Code as a Crime Scene»); считается она из обычного git log без всякого специального софта.
# Топ-30 самых часто меняемых файлов за последний год —
# первая половина карты горячих точек, считается за секунды
git log --since="1 year ago" --name-only --pretty=format: \
| grep -E '\.(py|go|ts|java|cs)$' \
| sort | uniq -c | sort -rn | head -30
# Сколько разных людей правило файл (знаниевый риск и bus factor)
git log --since="1 year ago" --format='%an' -- path/to/OrderService.java \
| sort -u | wc -l
Вывод, ради которого всё затевалось: сложный файл, который никто не трогает, — не проблема. Долг там есть, но процентов вы по нему не платите. Рефакторить надо пересечение «сложно» и «часто меняется» — там каждый час вложений возвращается многократно. Это радикально отличается от подхода «пройдёмся анализатором и починим все замечания».
Отдельно про закон Гудхарта: как только «покрытие тестами 80%» становится целью, оно перестаёт быть метрикой качества. Появляются тесты, вызывающие методы без единого ассерта. Покрытие — диагностический инструмент («в этом модуле 4%, посмотрим почему»), а не KPI.
Как продать рефакторинг бизнесу
Самый частый провал: разработчик приходит к менеджеру и говорит «код плохой, надо переписать, дайте месяц». Менеджер слышит: «месяц без результата, чтобы код стал красивее по мнению инженера». Отказ гарантирован, и он рационален — ему не дали ни одного аргумента в его системе координат.
Что работает.
1. Говорите последствиями, а не свойствами. Не «модуль оплаты — спагетти», а «за последние три месяца 6 из 11 инцидентов пришли из модуля оплаты; средняя фича в нём делается в 2,5 раза дольше, чем в остальном коде; каждый релиз мы держим тестировщика лишний день на ручном регрессе».
2. Считайте, но честно. Метод расчёта важнее цифр — и цифры ниже условные, для демонстрации метода, а не норматив:
Наблюдение (из трекера, не из ощущений):
доработки в модуле оплаты — 4 задачи в месяц
средний перерасход из-за состояния модуля — ~1,5 дня на задачу
→ 6 инженеро-дней в месяц теряется постоянно
Предложение:
рефакторинг ядра модуля — 12 инженеро-дней (оценка команды, ±50%)
ожидаемое сокращение перерасхода — примерно вдвое
Окупаемость:
экономия ~3 дня в месяц → вложение возвращается за ~4 месяца,
дальше идёт в плюс, пока модуль активно развивается
Риск бездействия:
инциденты в оплате = прямые потери выручки и репутации;
за квартал их было 6
Ключевая часть — не итоговая цифра, а то, что все входные данные взяты из трекера и мониторинга, а не из головы, и что вы явно показали разброс оценки. Менеджер, увидевший честный диапазон, доверяет больше, чем услышавший точное число.
3. Выбирайте формат вложения под ситуацию.
| Формат | Как выглядит | Когда работает | Риск |
|---|---|---|---|
| По пути (правило бойскаута) | улучшаем то, что и так трогаем | всегда, база | размывает PR, не решает крупное |
| Фиксированный процент | 15–20% ёмкости каждого спринта на долг | зрелая команда с доверием | процент тихо съедается фичами |
| Привязка к фиче | «эта фича делается 3 дня вместо 8, если сначала 2 дня на подготовку» | лучший вариант для крупного долга | требует уметь честно оценивать |
| Отдельный тикет с результатом | «сократить время сборки с 40 до 12 минут» | измеримый инфраструктурный долг | легко откладывается вечно |
| Fix-it week / технический спринт | вся команда на неделю уходит в долг | после тяжёлого квартала, перед пиком нагрузки | превращается в ритуал без приоритизации |
Наиболее устойчивый — привязка к фиче. Вы не просите денег «на красоту», вы выбираете более дешёвый способ сделать то, что бизнесу и так нужно.
4. Никогда не просите «переписать всё». Джоэл Спольски в классическом тексте Things You Should Never Do объяснил, почему большая перепись — самая опасная стратегическая ошибка: старый код уродлив не просто так, в нём годами накапливались исправления реальных багов, о которых вы уже не помните. Переписав, вы выбросите знание вместе с кодом и будете год ловить те же самые баги заново — а рынок в это время не подождёт.
Как отдавать долг: стратегии
Записать в реестр, вернуться,
если частота вырастет] B -- регулярно --> D{Есть тесты,
фиксирующие поведение?} D -- нет --> E[Сначала характеризующие тесты:
фиксируем то, что есть,
не то, что должно быть] E --> F D -- да --> F{Масштаб изменения} F -- локальный --> G[Рефакторинг по пути,
отдельным PR от фичи] F -- модуль --> H[Branch by abstraction:
интерфейс, две реализации,
переключение флагом] F -- подсистема --> I[Strangler Fig:
новое рядом,
трафик переводим по частям] F -- эта фича вообще нужна? --> J{Кто-то ею пользуется?} J -- нет --> K[Удалить.
Лучший рефакторинг — минус код] J -- да --> I G --> L[Замерить эффект:
время фичи, инциденты, CI] H --> L I --> L K --> L
Разберём инструменты из этой схемы.
Правило бойскаута («оставь стоянку чище, чем нашёл», популяризировано Робертом Мартином): трогаешь файл — улучши что-то маленькое рядом. Важная граница, которую джуны нарушают первым делом: не смешивайте рефакторинг и функциональные изменения в одном PR. Ревьюер не сможет отделить «переименовали переменную» от «поменяли условие», ревью займёт втрое дольше, а при откате придётся откатывать и полезное. Правильно: PR №1 — только рефакторинг, поведение не меняется, тесты те же и зелёные; PR №2 — фича.
Характеризующие тесты (Майкл Фезерс, «Working Effectively with Legacy Code»). Классический тупик: код надо менять, но тестов нет, а написать тесты нельзя, потому что код не тестируем. Выход — тесты, которые фиксируют текущее поведение, включая странное и, возможно, ошибочное. Их задача не проверить корректность, а поймать вас за руку, если вы что-то незаметно поменяли.
# Легаси: считает скидку, поведение никто не помнит целиком.
# Первый шаг — не улучшать, а зафиксировать. Значения получены
# прогоном текущего кода, а не выведены из требований.
import pytest
from legacy.pricing import calculate_discount
@pytest.mark.parametrize("total,is_vip,promo,expected", [
(1000, False, None, 0),
(1000, True, None, 50), # VIP: 5%
(5000, True, None, 500), # порог 3000 → 10%
(5000, True, "SUMMER", 750), # промо суммируется — так ли задумано?
(0, True, "SUMMER", 0),
(-100, False, None, 0), # отрицательная сумма не падает (!)
])
def test_characterization(total, is_vip, promo, expected):
assert calculate_discount(total, is_vip, promo) == expected
Комментарии в таком тесте — не украшение. Строка «промо суммируется — так ли задумано?» — это зафиксированный вопрос к продакту. Часто именно такие тесты вскрывают, что система годами вела себя не так, как все думали.
Branch by abstraction (описание Фаулера): вместо долгоживущей ветки вводим абстракцию над заменяемым куском, пишем новую реализацию рядом, переключаем флагом, потом удаляем старую. Всё это время код в main, релизы идут как обычно.
Strangler Fig (Фаулер, 2004) — та же идея на уровне системы: новая реализация растёт вокруг старой, трафик переводится по частям, старое отмирает по кускам. Отсылка к фикусу-душителю. Основной способ выбраться из большого легаси без «переписывания всего». Практика миграций данных под этим соусом (expand–contract) разбиралась в Релизе и эксплуатации, а сам архитектурный контекст — в Архитектурных паттернах.
Удаление. Самый недооценённый инструмент. Мёртвые фичи, забытые фича-флаги (флаг, включённый на 100% полтора года назад, — это уже не флаг, а мёртвая ветка кода), эндпоинты без вызовов, задания в кроне, которые ничего не делают. Прежде чем удалять — добавьте телеметрию и подождите цикл (месяц, квартал, для отчётности — год): вы удивитесь, что используется, и ещё больше — что нет. Минус тысяча строк кода не требует объяснений перед бизнесом и мгновенно снижает стоимость сопровождения.
Легаси: без презрения
«Легаси» в разговорах джунов обычно означает «код, который мне не нравится». Есть два определения получше.
Фезерс: легаси — это код без тестов. Не старый, не на устаревшем языке — а тот, который нельзя безопасно изменить, потому что нечем проверить, что вы ничего не сломали.
Второе, циничное и точное: легаси — это код, который приносит деньги. Если бы не приносил, его бы просто выключили, и он бы вас не мучил. Система, которую все ругают на кухне, обычно та, на которой держится выручка.
Отсюда — уважение к забору Честертона: прежде чем убрать непонятную конструкцию, выясните, зачем она поставлена. Странный if с проверкой на конкретного клиента почти наверняка появился после инцидента. sleep(200) в коде интеграции — потому что у партнёра гонка на их стороне. Первый рефлекс «убрать этот бред» — самый дорогой рефлекс в индустрии.
Как заходить в незнакомую систему, когда документации нет:
- Тесты — если есть, это лучшая существующая спецификация. Читайте их первыми.
git logиgit blame— история изменений плюс номера тикетов в сообщениях коммитов. Часто это единственный сохранившийся ответ на вопрос «почему так».- Логи и мониторинг прода — какие пути реально исполняются. Половина кода может быть недостижима.
- Люди — тот, кто помнит контекст, экономит вам недели. Спрашивать — не признак слабости, а рациональное использование ресурса; на дистанции это разбирается в Первых 90 днях.
- Маленькое безопасное изменение — до конца понять систему чтением невозможно. Проведите через весь конвейер что-то крошечное: заодно узнаете, как тут вообще устроены сборка, тесты и релиз.
Эволюция продукта: системы стареют
Отдельная тема, которую джуны не видят, потому что горизонт наблюдения короче.
Мейр Леман в 1970–80-х сформулировал законы эволюции ПО. Два из них стоит помнить наизусть:
- Закон непрерывного изменения: система, используемая в реальной среде, обязана меняться, иначе она становится всё менее полезной. Неизменный код не «стабилен», он устаревает относительно движущегося мира.
- Закон растущей сложности: по мере изменений сложность системы растёт, если специально не тратить силы на её снижение. То есть беспорядок — состояние по умолчанию, порядок требует энергии. Это не пессимизм, это термодинамика проектов.
Деприкейшн: как правильно убивать
Отключить то, чем пользуются, — отдельный навык. Резко выключенный публичный эндпоинт ломает клиентов, стоит денег и репутации. Взрослый процесс выглядит так.
Что здесь важно и почему:
- Сначала телеметрия, потом решения. Без данных о реальном использовании любой разговор об отключении — гадание. Логируйте не только «сколько вызовов», но и «кто вызывает».
- Заголовки
DeprecationиSunsetв HTTP-ответах (RFC 9745 и RFC 8594) — машиночитаемое предупреждение, которое клиент увидит в логах, даже если пропустил письмо. - Brownout — короткие плановые отключения по расписанию, объявленные заранее. Жестоко, но работает: те, кто игнорировал шесть писем, обнаруживаются за один день и, что важно, до финального отключения, когда ещё можно помочь.
- Удаление кода — отдельный шаг после отключения. Иначе мёртвый код останется в системе навсегда и будет пугать людей ещё три года.
Сроки в диаграмме — иллюстрация формы процесса, а не норматив. В публичном API для внешних разработчиков окно обычно от полугода до нескольких лет; во внутреннем сервисе с двумя известными потребителями — две недели и сообщение в чат.
Как понять, что фичу пора выключать
Симметричный вопрос к разработке: продукт растёт не только добавлением. Признаки мёртвой фичи — низкий или нулевой недельный охват, отсутствие обращений в поддержку (никто не жалуется, потому что никто не пользуется), стоимость поддержки выше вклада в выручку. Как считать продуктовые метрики и не обмануться — в Метриках продукта; разработчику здесь важно другое: инициировать разговор об удалении — нормальная и ценная инициатива, и она хорошо смотрится в разговоре о грейде.
Что всё это даёт лично вам
Прагматичный взгляд на карьеру, без морализаторства.
Поддержка — самый быстрый способ выучить систему. За неделю дежурства вы пройдёте по десятку модулей, увидите, как система ломается (а это говорит о ней больше, чем то, как она работает), и познакомитесь с людьми из соседних команд. Джун, отказывающийся от поддержки в пользу «интересных задач», обменивает быстрое обучение на медленное.
Работа с легаси — водораздел между грейдами. Написать новый сервис с нуля по понятной спецификации умеет крепкий джун. Аккуратно изменить систему, которую никто до конца не понимает, не сломав выручку, — навык мидла и сеньора. Подробности градации — в Грейдах.
Это отлично звучит на собеседовании. История «я нашёл, что 60% инцидентов приходят из одного модуля, посчитал перерасход, защитил рефакторинг, время фичи упало вдвое» бьёт любой рассказ про новую фичу: в ней есть данные, инициатива, коммуникация с бизнесом и измеримый результат. Как строить такие истории — в Как проходить собеседования.
И честное предупреждение о ловушке. Есть риск стать «тем, кто держит легаси»: незаменимым, ценным и застрявшим. Признаки — вы единственный, кто может это чинить; вас не отпускают на новые проекты именно поэтому; ваши достижения не видны никому, кроме тех, кто и так знает. Противоядия: документировать и передавать знание (незаменимость — уязвимость, а не актив), фиксировать результаты в измеримых величинах, регулярно обсуждать это на one-to-one. Механика такого разговора — в Как расти.
Типичные ошибки
- «Дайте нам спринт, мы всё перепишем». Почти всегда заканчивается системой, у которой те же проблемы плюс новые, плюс полгода без фич.
- Рефакторинг в одном PR с фичей. Убивает ревью и делает откат невозможным.
- Рефакторинг без тестов. Это не рефакторинг, а переписывание с закрытыми глазами. Рефакторинг по определению не меняет поведение — а как вы это докажете?
- Рефакторинг холодного кода. Красивая переработка модуля, который трогают раз в год, — потраченное время. Смотрите на карту горячих точек.
- «Техдолг» как отговорка. Иногда за фразой «тут техдолг» скрывается «мне лень разбираться» или «я бы написал иначе».
- Бэклог багов как свалка. Тысяча открытых тикетов, из которых 900 неактуальны, — это не бэклог, а способ не принимать решений. Регулярно закрывайте старое явным «won’t fix».
- Игнорирование обновлений зависимостей до момента, когда обновиться уже нельзя без переписывания.
- Личное отношение к чужому коду. Автор писал в других условиях, с другим знанием и другими сроками. Через год ваш сегодняшний код будет выглядеть так же — и это нормально.
- Молчаливое страдание. Если долг не оцифрован и не озвучен, для бизнеса его не существует. Ваша боль — не аргумент; цифры из трекера — аргумент.
Мини-итог
- После релиза начинается самая долгая и дорогая фаза жизни системы; сопровождение — это четыре разных занятия, и большая часть из них — развитие, а не починка.
- Линии поддержки экономят дорогое время, но глушат обратную связь; хорошие команды намеренно пробивают в фильтре окна.
- Severity — свойство дефекта, priority — решение бизнеса; путать их дорого. «Won’t fix» — честный статус.
- Техдолг по Каннингему — разрыв между текущим пониманием и кодом, а не «плохой код». Управляем только осознанный и записанный долг.
- Измеряйте последствия (доля незапланированной работы, lead time, change failure rate, флаки, возраст зависимостей), а не замечания анализатора.
- Вкладывайтесь в пересечение «сложно» × «часто меняется». Холодный сложный код можно не трогать.
- Отдавайте долг маленькими шагами: характеризующие тесты → branch by abstraction → strangler fig. Большая перепись — самая дорогая стратегия.
- Продукты стареют по законам Лемана; деприкейшн и удаление — такая же часть работы, как разработка.
- Поддержка и легаси — не наказание, а самый быстрый способ вырасти, если не застревать в роли единственного носителя знания.
Источники
- Ward Cunningham. The WyCash Portfolio Management System, OOPSLA'92 — первоисточник метафоры долга.
- Ward Cunningham. Debt Metaphor (пояснение автора).
- Martin Fowler. Technical Debt Quadrant, Strangler Fig Application, Branch By Abstraction.
- Michael Feathers. Working Effectively with Legacy Code — характеризующие тесты, швы, работа с непокрытым кодом.
- Martin Fowler. Refactoring, 2nd ed. — каталог рефакторингов; refactoring.com.
- Adam Tornhill. Your Code as a Crime Scene / Software Design X-Rays — анализ горячих точек по истории репозитория.
- Joel Spolsky. Things You Should Never Do, Part I — про большие переписывания.
- ISO/IEC 14764 — типы сопровождения ПО (корректирующее, адаптивное, совершенствующее, превентивное).
- M. M. Lehman. Laws of Software Evolution.
- RFC 9745 (заголовок
Deprecation) и RFC 8594 (заголовокSunset). - Google SRE Book, глава Eliminating Toil — про рутину, которую надо автоматизировать, а не терпеть.
Что дальше
Мы разобрали полный технический цикл: от требований до эксплуатации и старения продукта. Но всё это делают люди, и распределение ответственности между ними — отдельная тема, в которой джуны путаются больше всего: кто ставит задачу, кто решает, что важнее, кому эскалировать и кто в итоге отвечает за результат.
Кто есть кто в команде: роли, зоны ответственности, кто что решает