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

Стратегии модернизации легаси

Разработчику Архитектору
Теория данных (раздел 3)

Когда локальный рефакторинг (статья 3, методы рефакторинга) уже не спасает масштаб — нужна стратегия — как жить с монолитом годами, как выводить функции в новые сервисы, когда допустим полный rewrite (переписывание с нуля). Здесь — архитектурные приёмы и критерии выбора пути.

Назад: безопасные изменения. Итоги: статья 5.


С чего начать, если вы новичок

Представьте старый интернет-магазин — один большой сервер (монолит) на PHP 5, без тестов, автор уволился пять лет назад. Заказчик просит "мобильное приложение и оплату по СБП". Вы не можете за неделю "переписать всё на Go" — магазин должен продавать каждый день. Стратегия отвечает на вопрос: как менять систему по кусочкам, не останавливая бизнес.

ТерминПростыми словами
МодернизацияУлучшение старой системы: код, архитектура, инфраструктура, процессы
МонолитОдно приложение, в котором смешаны заказы, каталог, оплата
МикросервисОтдельная программа с одной зоной ответственности (например, только "пользователи")
Маршрутизатор"Диспетчер" запросов: решает, отдать запрос старому коду или новому
RewriteВыбросить старую реализацию и написать новую с чистого листа
ТрафикПоток запросов пользователей к API или сайту

Локальный рефакторинг (статья 3) — "починить комнату в доме". Стратегии из этой главы — "перестраивать этажи, пока люди живут в доме" или "строить новый корпус и переводить жильцов".

Когда читать эту главу

Вы уже понимаете, где в коде боль (статья 2), и умеете делать маленькие безопасные правки (статья 3).

Если задача — "вынести оплату в отдельный сервис за квартал" — вы здесь по адресу.


Пошаговый цикл работы с легаси

Универсальный цикл (повторяется на каждую крупную задачу):

  1. Оценка риска — стоимость простоя, критичность данных, регуляторика.
  2. Инвентаризация — код, конфиги, логи, люди, старые репозитории (статья 2).
  3. Анализ — зависимости, горячие точки, сценарии.
  4. Защитные тесты — characterization, API, e2e на критичном пути.
  5. Локальная правка — минимальный контекст, Mikado при необходимости.
  6. Наблюдаемость и откат — метрики, feature flags, быстрый rollback.
  7. Фиксация знаний — ADR, диаграмма, обновление runbook.

Шаги 4 (защитные тесты) и 7 (фиксация знаний) отличают управляемую эволюцию от разовых правок "на авось". Без тестов вы не узнаете, сломали ли оплату; без ADR и runbook следующий разработчик снова начнёт с нуля.


Разбор шагов для новичка

ШагЧто делают на практикеПример
Оценка рискаСпросить: что будет, если релиз упадёт на час?Интернет-магазин — критично; внутренний отчёт — терпимее
ИнвентаризацияСписок репозиториев, серверов, cron, "кто в курсе"Выяснили, что биллинг живёт в Excel + скрипте на сервере
АнализНарисовать, кто кого вызывает"Оплата" тянет за собой 12 модулей — выносить последней
Защитные тестыЗафиксировать текущее поведениеCharacterization на расчёт скидки VIP
Локальная правкаМаленький PR в одной зонеВынести расчёт скидки в отдельный класс
НаблюдаемостьМетрики, логи, флаг "откатить"5% трафика на новый сервис, остальное — монолит
Фиксация знанийADR: "почему вынесли скидки в сервис X"Ссылка в Confluence + файл в docs/adr/

Strangler Fig (инжир-душитель)

Strangler Fig (буквально "душитель-инжир") — название от тропического растения: лиана обвивает дерево и со временем заменяет его своим стволом. В ПО та же идея — новая реализация постепенно обрастает вокруг старой, трафик и ответственность переключаются по частям, монолит сжимается, пока его можно отключить.

Подробная статья с этапами миграции: Strangler Fig в разделе архитектуры.


Суть по шагам (история "профиль пользователя")

  1. Сегодня мобильное приложение ходит на https://shop.example/api/... — всё обрабатывает монолит.
  2. Команда пишет новый сервис users-service с тем же JSON на выходе.
  3. Перед монолитом ставят маршрутизатор (Nginx, Kong, Envoy, облачный API Gateway).
  4. Правило: запросы GET/PUT /api/users/*users-service, всё остальное → монолит.
  5. Пользователь не замечает смены: URL тот же, ответ похож.
  6. Через месяц выносят "каталог", потом "корзину"; монолит уменьшается.
  7. Когда в монолите остаётся мало — его выключают или оставляют как архив.

Маршрутизатор — единая "входная дверь". Клиенты (сайт, приложение, партнёры) по-прежнему стучатся в один адрес; внутри запрос перенаправляют.

Обратная совместимость — старые клиенты продолжают работать без срочного обновления: те же коды ошибок, те же поля JSON (или аккуратное версионирование /api/v2).


Пример маршрутизации (Nginx)

# новый сервис забирает только users
location /api/users {
proxy_pass http://users-service:8080;
}

# всё остальное — пока в монолит
location /api/ {
proxy_pass http://legacy-monolith:8080;
}

Параллельный прогон (shadow / compare)

Перед тем как отдавать ответ клиенту из нового сервиса, команду мучает вопрос: "а вдруг новый код считает скидку иначе?"

Shadow-режим (теневой прогон):

  1. Запрос приходит в маршрутизатор.
  2. Ответ пользователю по-прежнему формирует монолит (как сейчас).
  3. Копия запроса тихо уходит в новый сервис; его ответ сравнивают с монолитом в логах или в отдельной системе.
  4. Расхождения чинят, пока совпадение не станет приемлемым.
  5. Только потом переключают боевой ответ на новый сервис (часто через feature flag — процент трафика).

Так снижают риск "большого переключения в пятницу вечером".

Strangler — осознанный план выноса, а не разовая обёртка. Сначала выбирают bounded context (ограниченный контекст в DDD): кусок предметной области с понятными границами. Часто первым выносят профиль или справочники — мало связей с оплатой. Оплату и склад оставляют на потом — там самые жёсткие зависимости. Подробнее — Strangler в архитектуре.


Anti-Corruption Layer (слой антикоррупции)

Anti-Corruption Layer (ACL) — "переводчик" между вашим аккуратным кодом и чужой моделью легаси. Слово corruption здесь про заражение домена — если вы тащите в новый сервис поля вроде CustNo, LoyaltyCode = "V" и магические числа, вся новая команда начнёт думать "так и надо" — и технический долг переедет в микросервис.

DTO (Data Transfer Object) — простая "коробка" с полями для передачи по сети, без бизнес-логики.

Правило для новичка — новый домен (заказ, клиент, тариф) описываете своими типами; один модуль-адаптер знает про уродливый SOAP/XML/таблицу TBL_CUST_1998.

Без ACLС ACL
OrderService напрямую парсит XML легасиOrderService видит только Order и CustomerId
Переименование поля в монолите ломает 20 файловПравите адаптер в одном месте
Тесты требуют поднять весь монолитТесты подменяют ILegacySoapClient заглушкой

Когда новый код обязан говорить с легаси:

  • легаси отдаёт "странный" DTO или XML;
  • ACL приводит к вашим типам и правилам;
  • изменения в монолите локализованы за адаптером.

Код ITЗагрузка примера кода…

ACL часто живёт рядом со Strangler: новый сервис + адаптер к старому API.


Clean room (чистая комната)

Clean room пришёл из микроэлектроники и судебных споров о интеллектуальной собственности (IP): компания A хочет сделать чип "как у B", но без копирования исходников B. Решение — разделить людей на две "комнаты":

  • в грязной (dirty) комнате изучают чужой продукт и пишут спецификацию поведения ("при входе X система делает Y");
  • в чистой (clean) комнате пишут новую реализацию только по спецификации, без доступа к исходникам чужого кода.

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

В легаси адаптируют так:

ЗонаКтоЧто делает
"Грязная" (dirty)аналитики, reverse engineersизучают старую систему, пишут спецификации поведения, API, форматы
"Чистая" (clean)разработчики заменыпишут новый код без просмотра исходников старого — только по спецификации

Чистая комната — это clean room, а не наоборот. "Грязная" — где легаси; "чистая" — где новая реализация без переноса стиля и багов копипастой.

Применимо, когда:

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

На практике полный clean room редок; чаще — упрощённый вариант — один человек читает легаси, другой проектирует API по документу, ревью ловит "утечку" старых имён в новый код.


Защитный слой с внешним легаси

Если вы не владеете кодом вендора — "защитный слой" у вас на границе:

  • строгий контракт (OpenAPI, schema registry);
  • таймауты, circuit breaker, идемпотентность;
  • логирование запрос/ответ (без секретов) для расследований;
  • тесты на вашей стороне по записанным фикстурам.

Это не Strangler внутри монолита, но та же идея: не пускать чужую модель глубоко в домен.


Инструменты (краткий каталог)

Инструменты не заменяют понимание (статья 2), но ускоряют карту местности:

  • Зависимости: NDepend (C#), ArchUnit/jQAssistant (Java), dependency-cruiser (JS/TS).
  • Качество и долг: SonarQube, CodeClimate, Snyk Code.
  • Поиск по коду: Sourcegraph, OpenGrok, CodeQL.
  • Декомпиляция: Ghidra, dotPeek, JD-GUI.
  • API без исходников: mitmproxy, Charles → черновик OpenAPI в Postman/Stoplight.
  • Диаграммы: PlantUML, Mermaid, Structurizr.

Выбирайте 2–3 инструмента под стек, не весь список сразу.


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

Полный rewrite (переписывание с нуля) — отключить старую систему и заменить новой за один или несколько больших релизов. Это один из самых рискованных ходов: новая система почти наверняка получит новые баги; старые годами обкатаны бизнесом и регуляторами.

Исторический пример из индустрии: проекты, где команда годами писала "версию 2.0", пока "версия 1" всё ещё приносила деньги — и в итоге v2 опаздывала, а v1 раздувалась. Поэтому по умолчанию выбирают Strangler, а rewrite оставляют для жёстких случаев.

Вопрос на совещании

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

Без ответов это rewrite без стратегии.


Rewrite чаще оправдан, если

  1. Технологический тупик — нельзя запустить на поддерживаемой ОС/железе, порт нереален (край: 16-bit DOS).
  2. Лицензия — нельзя продолжать использовать компонент и нельзя заменить точечно.
  3. Потеря знаний — восстановление дороже controlled rewrite и бизнес готов к длительному параллельному существованию двух систем.
  4. Цели бизнеса недостижимы на текущей архитектуре (масштаб, география, регуляторика) даже со Strangler.

В остальных случаях

Предпочтительны Strangler, ACL, рефакторинг с тестами. Даже "старый" монолит можно сужать годами без остановки выручки.


Кривая стоимости изменений (интуиция)

Стоимость правки
^
| **** без тестов / без карты зависимостей
| **
| **___________ после тестов + диаграмм + ACL
+------------------------> время

Инвестиция в тесты и карту окупается скоростью следующих релизов.


Баланс — баги в легаси и новые фичи

Практичное правило для планирования:

  • Критичный инцидент — чиним с characterization-тестом, без побочного рефакторинга.
  • Фича в легаси-модуле — закладываем время на шов/тест; иначе фича удвоит долг.
  • 20% спринта (или отдельный поток) — погашение долга в зоне, которую всё равно трогаете.

В госсекторе и финтехе добавляется слой регламентов: "странный" код может быть следствием аудита, не лени.


Связь с другими разделами

ТемаГде углубляться
Strangler, монолит → микросервисыdesign/2125, разделение монолита
Тесты, CI7-05 Тестирование
Техдолг в спринтах7-03 Методология
Читаемость при правках7-10 Культура кода

Дальше — итоги раздела и чек-лист.