Новое в журнале. Когда переписать проект с нуля выгоднее рефакторинга
Принципы и архитектура

Когда переписать проект с нуля выгоднее рефакторинга

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

6 ноября 202616 мин чтенияПрактикам
Когда переписать проект с нуля выгоднее рефакторинга
4 ситуациикогда переписывание дешевле рефакторинга
4 шагаStrangler Fig без остановки продакшена

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

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

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

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

Рефакторинг или переписывание — в чём разница на уровне решения

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

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

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

Классический предостерегающий пример — переписывание браузера Netscape в конце 1990-х, которое Джоэл Спольски разобрал в эссе «Things You Should Never Do»: команда остановила выпуск новых версий почти на три года ради полного переписывания с нуля, и за это время конкуренты успели забрать значительную часть рынка. Урок не в том, что переписывание всегда ошибка, а в том, что переписывание «всем составом и сразу», без поэтапного выпуска промежуточных результатов, несёт риск, сопоставимый с риском самой проблемы, которую оно должно решить.

Какие признаки говорят, что рефакторинг больше не спасает

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

  • Любая небольшая фича требует согласованных правок в пяти и более формально не связанных местах кода
  • Разработчики открыто избегают определённых файлов и просят в ревью «не трогать этот модуль без крайней необходимости»
  • Тесты либо отсутствуют на критичных путях, либо настолько хрупкие, что падают от изменений, логически с ними не связанных
  • Онбординг нового разработчика в проблемный модуль занимает недели вместо дней, типичных для остальной кодовой базы
  • Архитектурное решение, заложенное в основании системы — например, синхронная монолитная модель данных, — прямо противоречит текущим требованиям к нагрузке или масштабированию
  • Команда уже проводила точечный рефакторинг этого участка два-три раза, но метрики вроде цикломатической сложности и динамики тестового покрытия не улучшаются между итерациями

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

Если несколько из этих сигналов уже отслеживаются в рамках учёта технического долга (см. материал о том, как его считать), сравнение в динамике за два-три квартала почти всегда даёт однозначный ответ быстрее, чем команда успевает годами спорить об этом на словах, опираясь только на общее ощущение.

Когда переписывание выгоднее рефакторинга — четыре ситуации

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

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

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

Сравнение рефакторинга и переписывания

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

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

Strangler Fig Pattern — как снизить риск при переписывании

Strangler Fig Pattern — новый модуль постепенно вытесняет старый монолитный блок

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

  1. Выделить границы модуля. Определить один логически замкнутый кусок функциональности — например, биллинг или авторизацию, — который можно переписать и протестировать независимо от остальной системы.
  2. Поднять новый код рядом со старым за прокси или роутингом. Входящие запросы по-прежнему идут в старую систему, но появляется слой, который умеет направить часть трафика в новую реализацию без изменений на стороне клиентов. На практике роль такого прокси чаще всего играет уже существующий API-гейтвей или балансировщик нагрузки — заводить отдельный сервис-маршрутизатор ради одной миграции обычно избыточно, если в инфраструктуре уже есть точка, через которую проходит весь входящий трафик.
  3. Переключать трафик частями. Сначала на новую реализацию переводят долю трафика или отдельный сегмент пользователей, сверяют результат со старой системой и постепенно увеличивают долю до ста процентов.
  4. Выключить старый код, когда новый покрыл всё. Только после того как весь трафик стабильно обрабатывается новой реализацией в течение разумного периода, старый модуль удаляют — а не раньше, из опасения, что откат понадобится снова.

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

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

Как не потерять функциональность и данные при переписывании

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

Тесты-характеристики (characterization tests) фиксируют фактическое поведение старой системы на реальных и граничных входных данных, без оценки, правильное это поведение или нет. Их задача — дать объективный критерий, повторяет ли новая реализация старую, а не полагаться на память команды о том, как система «должна» работать.

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

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

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

Сколько стоит отказ от решения

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

Пример: если за месяц команда тратит 60 часов на разбор инцидентов в legacy-модуле биллинга при средней стоимости часа 2 500 ₽, прямые издержки отказа от решения составляют 150 000 ₽ в месяц — то есть 1,8 млн ₽ в год, не считая отложенной скорости разработки новых функций, которую отдельно посчитать сложнее, но которая обычно превышает прямые издержки.

Отдельно стоит считать не только часы на инциденты, но и упущенную скорость запуска нового функционала: если временное решение в legaсy-модуле блокирует выход платной функции на две недели, а ожидаемая выручка от неё — 500 000 ₽ в месяц, цена задержки входит в расчёт отдельной строкой и чаще всего оказывается весомее, чем часы, потраченные на сам инцидент.

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

Чек-лист перед тем, как выбрать подрядчика на переписывание

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

  • Техническое задание на уровне поведения, а не реализации. Что система должна делать для пользователя и смежных систем, а не как это реализовано сейчас в коде — реализацию как раз меняют.
  • Явные границы системы. Какие интеграции, базы данных и внешние сервисы затрагивает переписывание, а какие остаются неизменными и просто продолжают получать те же запросы.
  • Критерии приёмки по каждому модулю. Конкретные сценарии и граничные случаи, на которых новая система должна вести себя так же, как старая — лучше в виде тестов-характеристик, а не текстового описания.
  • План поэтапной выкатки. Будет ли миграция идти через Strangler Fig по модулям или единым релизом, и кто отвечает за решение об откате на каждом этапе.
  • Доступ к знаниям о текущей системе. Контакт с человеком, который может ответить на вопрос «а почему это работает именно так», даже если он больше не пишет код в проекте.
  • Выделенное окно на тестовую миграцию с данными, похожими на продовые по объёму и структуре. Без таких данных тесты-характеристики проверяют только придуманные сценарии и пропускают именно те граничные случаи, которые чаще всего всплывают уже после запуска у реальных пользователей.

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

Частые вопросы

Как понять, что мы не поторопились с решением переписать систему?

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

Можно ли переписать только часть системы, а не всё целиком?

Да, и это обычно правильнее полного переписывания. Strangler Fig Pattern как раз про это: вы выделяете границы одного модуля, переписываете его, переключаете трафик и повторяете для следующего, вместо того чтобы останавливать всю разработку на месяцы ради одного большого релиза.

Сколько времени обычно занимает переписывание проекта с нуля?

Зависит от масштаба и выбранной стратегии: точечная замена одного модуля через Strangler Fig может занять 4-8 недель, полное переписывание среднего по размеру сервиса — от нескольких месяцев до года. Любая оценка «за две недели перепишем всё» для системы с реальной бизнес-логикой почти всегда занижена.

Что делать, если переписывание уже началось, но буксует?

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

Нужно ли тестовое покрытие старой системы перед тем, как её переписывать?

Да, хотя бы в виде тестов-характеристик (characterization tests), которые фиксируют текущее поведение системы как есть, без оценки, правильное оно или нет. Без них нет способа проверить, что новая реализация действительно повторяет старую, а не теряет нюансы бизнес-логики, которые никто не документировал.

Что опаснее — недооценить сложность переписывания или переоценить её?

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

Как выбрать между своей командой и внешним подрядчиком для переписывания?

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

Что произойдёт, если просто продолжать точечно латать систему, не выбирая ни один из вариантов?

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

Код, который не стыдно передать

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

Случайный разбор