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

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

Разработчику Архитектору

Когда локальный рефакторинг (статья 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 приводит к вашим типам и правилам;
  • изменения в монолите локализованы за адаптером.
// Новый домен не знает про LegacyCustomerRecord
public sealed class CustomerAdapter : ICustomerLookup
{
private readonly ILegacySoapClient _legacy;

public async Task<Customer> FindAsync(CustomerId id, CancellationToken ct)
{
var raw = await _legacy.GetCustomerAsync(id.Value, ct);
return new Customer(
id: new CustomerId(raw.CustNo),
name: raw.FullName?.Trim() ?? "Unknown",
tier: MapTier(raw.LoyaltyCode) // странные коды — только здесь
);
}

private static Tier MapTier(string? code) => code switch
{
"V" => Tier.Vip,
"G" => Tier.Gold,
_ => Tier.Standard
};
}

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 Культура кода

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


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).