{ "index": 144, "slug": "editorial-2024-01-practice-legacy-modernization", "title": "Модернизация legacy-системы: как выбрать безопасный шов", "excerpt": "Полное переписывание редко начинается с доказанной границы. Разбираем, как выделить одну операцию, описать её поведение, подключить новый путь через адаптер и не спутать возврат маршрута с восстановлением данных.", "contentHtml": "
Симптом заметен по backlog: задача называется «переписать расчёт заказа», но не содержит одного входа и одного результата. Внутри старого модуля смешаны HTTP-обработчик, скидки, запись статуса, письмо и вызовы соседних систем. Команда создаёт новый сервис, а через несколько недель не знает, какая часть поведения уже перенесена. На переключении обнаруживаются редкие правила и побочные эффекты. Цена ошибки — задержка релиза, двойная запись, потерянное письмо или откат, который меняет маршрут, но не возвращает данные.
\nБезопасная модернизация начинается не с новой технологии. Она начинается с измеримого шва. Шов — это одна операция с названным потребителем, допустимым входом, наблюдаемым результатом, владельцем состояния и понятным способом остановки. Он не обещает сохранить всю систему. Он ограничивает первый риск так, чтобы расхождение можно было увидеть и разобрать.
\nПапка, класс или новый микросервис сами по себе не образуют границу. Граница появляется там, где можно задать проверяемый контракт. Для HTTP это может быть один endpoint. Для очереди — один тип сообщения и ключ повторной обработки. Для интерфейса — одно действие пользователя и один command.
\nОпишите шов пятью строками: consumer, input, response, effect и owner. Consumer показывает, кто вызывает операцию. Input фиксирует обязательные поля, форматы и повтор. Response описывает статус и значимые поля. Effect перечисляет запись, сообщение, инвалидацию кеша или отсутствие изменения. Owner принимает решение при расхождении. Неизвестный эффект помечайте как unknown. Не заменяйте его предположением о parity.
\nШирокая формулировка вроде «вся корзина» скрывает слишком много решений. Сузьте её до preview или confirm. Preview обычно легче сделать без изменения состояния. Confirm имеет побочные эффекты и требует отдельного контракта повторов, ошибок и идемпотентности. Эти операции могут использовать общие данные, но не должны попадать в один первый rollout только потому, что так выглядит удобнее.
\nНиже — ограниченный учебный пример. Он не описывает конкретную production-систему и не доказывает совместимость с настоящим legacy-кодом. Пусть старый модуль рассчитывает цену заказа по запросу POST /quote. Клиент ожидает сумму, срок действия предложения и код ошибки. При POST /confirm модуль резервирует товар и отправляет событие в очередь. Начнём только с /quote: у него нет записи заказа, а результат можно сравнить до переключения.
type QuoteInput = {\n productId: string\n quantity: number\n customerTier?: 'base' | 'plus'\n}\n\ntype QuoteResult =\n | { status: 'ok'; total: number; expiresAt: string }\n | { status: 'rejected'; code: 'INVALID_QUANTITY' | 'NOT_AVAILABLE' }\n\nfunction routeQuote(input: QuoteInput, mode: 'legacy' | 'candidate') {\n if (mode === 'candidate') return modernQuoteAdapter(input)\n return legacyQuote(input)\n}\nКод показывает форму границы, а не готовую реализацию. Адаптер должен преобразовать вход в формат нового пути и вернуть объявленную форму ответа. Он не должен заодно писать в общую базу, отправлять письмо или вызывать confirm. Если для расчёта ему нужен скрытый глобальный флаг или побочный вызов, это факт для карты зависимости. Его нельзя прятать за универсальным gateway.
Для /quote сравните не все байты ответа, а заранее названные инварианты: статус, итоговую сумму, срок действия и код отказа. Проверьте округление, отсутствие товара, нулевое и отрицательное количество, повторный запрос и тайм-аут зависимости. Учебные данные должны быть явно ограничены. Они показывают, как составить проверку; они не заменяют ответы старого модуля, историю инцидентов и реальные ограничения среды.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| «Перенесём весь расчёт» | Единицей работы стала архитектура, а не операция | Назвать один consumer, input, response и effect | Сузить шов до quote или отдельного варианта результата |
| Зелёный тест локально | Тест не знает скрытые вызовы и правила legacy | Сверить valid, invalid и repeat cases с источником поведения | Оставить тест учебным и собрать отдельное evidence |
| Новый сервис копирует схему | Схема не фиксирует порядок эффектов и повтор | Проверить статус, идемпотентность, ошибки и время ответа | Добавить compatibility record или уменьшить scope |
| Нет владельца маршрута | Некому принять решение при расхождении | Назначить owner решения и owner состояния | Не включать новый путь до явного решения |
| Rollback означает «вернуться назад» | Маршрут смешан с уже изменёнными данными | Разделить route return, provider effect и data recovery | Описать только обратимое действие; остальное считать отдельной работой |
Маршрутизатор полезен не названием паттерна, а местом принятия решения. В нём видны режимы legacy-only, ограниченное предложение нового пути и возврат к legacy. Переключатель должен жить на границе операции. Если флаг разбросан по внутренним вызовам, один запрос может частично пройти по старому пути и частично по новому. Тогда сравнение теряет смысл.
Постепенное вытеснение не означает автоматическую безопасность. AWS описывает strangler fig как способ постепенно заменять отдельную функциональность работающего монолита. Это снижает размер изменения, но не проверяет бизнес-смысл полей и не отменяет работу с состоянием. Для каждой операции всё равно нужны owner, наблюдаемый сигнал, окно проверки и правило остановки.
\nСовместимость часто ломается не на успешном запросе. Старый код может округлять сумму после скидки, считать повтор безопасным или возвращать особый код при отсутствии товара. Новый сервис легко выдаёт правдоподобный 200, но записывает другое значение. Поэтому проверка должна начинаться с отказов, тайм-аутов и повторов.
У операций с состоянием есть дополнительное ограничение. Возврат маршрута не удаляет созданную запись, не отменяет платёж и не отзывает сообщение у внешнего провайдера. Для такого эффекта нужны идентификатор операции, владелец сверки и отдельное правило компенсации. Если компенсация не доказана, не называйте rollout обратимым. Выберите read-only или preview-шов либо оставьте эффект у legacy.
\nОграничение касается и данных сравнения. Synthetic-пример, мок или локальная база проверяют форму адаптера. Они не дают оснований заявлять сохранённое поведение реального модуля. Не выдавайте зелёный тест за результат трафика и не переносите вывод с одной популяции на другую. Если новый путь видит только простые заказы, он ещё не проверен на скидки, возвраты и повторные запросы.
\nПервый шов готов к ограниченному рассмотрению, если другой инженер может без устного контекста показать: один вход, один ожидаемый результат, список значимых эффектов, владельца решения, набор valid/invalid/repeat cases, источник каждого факта и известный legacy-маршрут. Для маршрута есть проверяемый возврат. Для необратимых данных отдельно названо, что возврат не покрывает.
\nЕсли хотя бы один из этих пунктов неизвестен, решение не провалилось. Оно ещё не достигло границы, на которой безопасно менять путь. Сузьте операцию, соберите недостающее доказательство или оставьте legacy владельцем. Готовность здесь означает не «новый сервис написан», а «расхождение можно обнаружить, остановить и объяснить».
\n