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

Почему старый код без тестов называют легаси и что с этим делать

Майкл Фезерс в книге "Working Effectively with Legacy Code" дал определение, которое сняло лишние споры: легаси это код без тестов. Возраст, язык и стиль не важны: если изменение нельзя проверить за минуты, код сопротивляется любой работе. Через пять лет после релиза система обрастает свойствами, которые не читаются из исходников: обходы особых дней в биллинге, молчаливые договорённости с отделом поддержки, поведение, к которому привыкли клиенты за годы.

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

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

Знания о причинах решений теряются быстрее самого кода. Архитектурная запись решения, одна страница с контекстом, принятым решением и последствиями, снимает вечный вопрос "почему здесь так". Через пять лет такая страница экономит неделю чтения исходников, а досье из десятка записей описывает систему честнее устаревшей документации.

Реальная цена переписывания системы с чистого листа

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

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

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

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

Тестовая страховка перед любым изменением унаследованного кода

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

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

Сам тест умещается в несколько строк, сравнение зафиксировано, любая смена поведения видна мгновенно:

def test_report_totals_unchanged():
    result = build_report(load("orders_2020.json"))
    assert json.dumps(result, sort_keys=True) == snapshot("report_2020.txt")

Проверка покрытия встроена в каждый стек одной командой:

pytest --cov=app --cov-report=term
npx jest --coverage

Мутационное тестирование усиливает картину: StrykerJS для JavaScript, Infection для PHP и PIT для Java вносят мелкие поломки в код и смотрят, ловит ли их тест. Выжившая мутация означает дыру в проверках, и список дыр честнее любого процента.

Инструменты статического анализа и аудит зависимостей на каждый день

Старый проект сначала просвечивают, не меняя ни строчки. Линтеры дают список проблем за вечер: ESLint с плагином для TypeScript, PHPStan с уровнями строгости, RuboCop для Ruby, Clippy для Rust. SonarQube добавляет метрику когнитивной сложности, она честнее цикломатической, потому что учитывает вложенность и прерывания потока, а не только число ветвей. Детектор jscpd ищет скопированные блоки: дубликаты в старом коде живут годами и расходятся понемногу, пока разница не станет багом.

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

Археология хранилища версий отвечает на вопрос, почему код стал таким:

git log --follow --oneline -- src/legacy/billing.php
git blame -L 120,180 src/legacy/billing.php

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

Состав и свежесть зависимостей смотрят тремя командами:

npm outdated
npm audit --omit=dev
composer outdated --direct

Боты Renovate и Dependabot превращают аудит в поток мелких запросов на слияние: обновление одной зависимости за раз, автоматические тесты, человеку остаётся кнопка. Ручная ревизия раз в квартал всегда проигрывает автомату, который проверяет каждый день.

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

Приёмы малых шагов и постепенное удушение старой системы

Фундамент заложил Мартин Фаулер в книге "Рефакторинг": изменение внутренней структуры крошечными шагами, каждый шаг компилируется и проходит тесты, вся операция занимает минуты, а не недели. Первое издание каталога вышло в 1999 году, второе в 2018-м, с примерами на JavaScript. Приёмы просты: извлечь функцию, переименовать переменную, удалить дубликат, свернуть запутанную условную логику. Сложность не в приёмах, а в дисциплине не смешивать чистку с правкой логики в одном наборе изменений.

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

Паттерн удушения strangler fig Мартин Фаулер описал в 2004 году: старую систему не удаляют, а обволакивают новой. Новые экраны и обработчики встают поверх, маршрутизация переводит потоки по частям, старые модули отрезаются по одному, и через год-два от исходной системы остаётся пустая оболочка. Ветка по абстракции и параллельное изменение держат порядок в переходный период: интерфейс вводится первым, обе реализации живут рядом, вызовы переезжают постепенно, старая реализация удаляется последней. Флаги функций завершают набор: новая логика едет в продакшн выключенной, включение для одного процента пользователей рассказывает о поведении больше, чем месяц ручных проверок.

Обновление рантайма и зависимостей без остановки разработки

Через пять лет рантайм сам становится легаси. Python 2 перестал получать исправления 1 января 2020 года, и организации с крупными кодовыми базами тянули миграцию на третью версию годами. Node.js держит каждую LTS-ветку 30 месяцев: ветка 14 закончилась в апреле 2023 года, ветка 16 в сентябре 2023-го, ветка 18 в апреле 2025-го. AngularJS, первая линия фреймворка, лишился расширенной поддержки в конце 2021 года, и проекты на нём оказались заперты на старом стеке JavaScript. Дата окончания поддержки известна заранее, и у команды всегда есть выбор: мигрировать по плану или тушить пожар в последний месяц.

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

npx tsc --noEmit
npm ci && npm test

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

Воспроизводимость сборки держит lock-файл: package-lock.json хранится в хранилище версий, команда npm ci ставит точные копии зависимостей, и сервер сборки не привозит сюрпризов, которых не было на ноутбуке. Для старого проекта одно различие между npm install и npm ci заслуживает отдельной строки в правилах команды.

План работ на первый месяц в живом проекте

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

Короткий чек-лист выглядит так:

  1. измерить текущее состояние и выбрать двадцать горячих файлов, а не пытаться покрыть всё сразу;
  2. навесить тест-характеризации на поведение, которое тронут в первую очередь;
  3. включить линтеры и ботов обновлений в конвейер, чтобы дно не опускалось обратно;
  4. двигаться шагами из каталога рефакторинга и паттерна удушения, не смешивая чистку с новыми требованиями.

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