8 lines
17 KiB
JSON
8 lines
17 KiB
JSON
{
|
||
"index": 144,
|
||
"slug": "editorial-2024-01-practice-legacy-modernization",
|
||
"title": "Модернизация legacy-системы: как выбрать безопасный шов",
|
||
"excerpt": "Полное переписывание редко начинается с доказанной границы. Разбираем, как выделить одну операцию, описать её поведение, подключить новый путь через адаптер и не спутать возврат маршрута с восстановлением данных.",
|
||
"contentHtml": "<p>Симптом заметен по backlog: задача называется «переписать расчёт заказа», но не содержит одного входа и одного результата. Внутри старого модуля смешаны HTTP-обработчик, скидки, запись статуса, письмо и вызовы соседних систем. Команда создаёт новый сервис, а через несколько недель не знает, какая часть поведения уже перенесена. На переключении обнаруживаются редкие правила и побочные эффекты. Цена ошибки — задержка релиза, двойная запись, потерянное письмо или откат, который меняет маршрут, но не возвращает данные.</p>\n<p>Безопасная модернизация начинается не с новой технологии. Она начинается с измеримого шва. Шов — это одна операция с названным потребителем, допустимым входом, наблюдаемым результатом, владельцем состояния и понятным способом остановки. Он не обещает сохранить всю систему. Он ограничивает первый риск так, чтобы расхождение можно было увидеть и разобрать.</p>\n<h2>Что именно нужно выделить</h2>\n<p>Папка, класс или новый микросервис сами по себе не образуют границу. Граница появляется там, где можно задать проверяемый контракт. Для HTTP это может быть один endpoint. Для очереди — один тип сообщения и ключ повторной обработки. Для интерфейса — одно действие пользователя и один command.</p>\n<p>Опишите шов пятью строками: consumer, input, response, effect и owner. Consumer показывает, кто вызывает операцию. Input фиксирует обязательные поля, форматы и повтор. Response описывает статус и значимые поля. Effect перечисляет запись, сообщение, инвалидацию кеша или отсутствие изменения. Owner принимает решение при расхождении. Неизвестный эффект помечайте как unknown. Не заменяйте его предположением о parity.</p>\n<p>Широкая формулировка вроде «вся корзина» скрывает слишком много решений. Сузьте её до preview или confirm. Preview обычно легче сделать без изменения состояния. Confirm имеет побочные эффекты и требует отдельного контракта повторов, ошибок и идемпотентности. Эти операции могут использовать общие данные, но не должны попадать в один первый rollout только потому, что так выглядит удобнее.</p>\n<h2>Учебный пример: quote и confirm</h2>\n<p>Ниже — ограниченный учебный пример. Он не описывает конкретную production-систему и не доказывает совместимость с настоящим legacy-кодом. Пусть старый модуль рассчитывает цену заказа по запросу <code>POST /quote</code>. Клиент ожидает сумму, срок действия предложения и код ошибки. При <code>POST /confirm</code> модуль резервирует товар и отправляет событие в очередь. Начнём только с <code>/quote</code>: у него нет записи заказа, а результат можно сравнить до переключения.</p>\n<pre><code>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}</code></pre>\n<p>Код показывает форму границы, а не готовую реализацию. Адаптер должен преобразовать вход в формат нового пути и вернуть объявленную форму ответа. Он не должен заодно писать в общую базу, отправлять письмо или вызывать <code>confirm</code>. Если для расчёта ему нужен скрытый глобальный флаг или побочный вызов, это факт для карты зависимости. Его нельзя прятать за универсальным gateway.</p>\n<p>Для <code>/quote</code> сравните не все байты ответа, а заранее названные инварианты: статус, итоговую сумму, срок действия и код отказа. Проверьте округление, отсутствие товара, нулевое и отрицательное количество, повторный запрос и тайм-аут зависимости. Учебные данные должны быть явно ограничены. Они показывают, как составить проверку; они не заменяют ответы старого модуля, историю инцидентов и реальные ограничения среды.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>«Перенесём весь расчёт»</td><td>Единицей работы стала архитектура, а не операция</td><td>Назвать один consumer, input, response и effect</td><td>Сузить шов до <code>quote</code> или отдельного варианта результата</td></tr><tr><td>Зелёный тест локально</td><td>Тест не знает скрытые вызовы и правила legacy</td><td>Сверить valid, invalid и repeat cases с источником поведения</td><td>Оставить тест учебным и собрать отдельное evidence</td></tr><tr><td>Новый сервис копирует схему</td><td>Схема не фиксирует порядок эффектов и повтор</td><td>Проверить статус, идемпотентность, ошибки и время ответа</td><td>Добавить compatibility record или уменьшить scope</td></tr><tr><td>Нет владельца маршрута</td><td>Некому принять решение при расхождении</td><td>Назначить owner решения и owner состояния</td><td>Не включать новый путь до явного решения</td></tr><tr><td>Rollback означает «вернуться назад»</td><td>Маршрут смешан с уже изменёнными данными</td><td>Разделить route return, provider effect и data recovery</td><td>Описать только обратимое действие; остальное считать отдельной работой</td></tr></tbody></table>\n<figure><img src=\"/assets/editorial/2024/legacy-modernization-2024-strangler-map.svg\" alt=\"Схема безопасного шва: клиент отправляет запрос в маршрутизатор, который оставляет legacy основным путём и направляет ограниченный вариант в новый адаптер\"><figcaption>Шов удерживает решение о маршруте на границе операции. Иллюстрация учебная: она не показывает реальный трафик, базу или подтверждённую совместимость.</figcaption></figure>\n<h2>Почему нужен маршрутизатор</h2>\n<p>Маршрутизатор полезен не названием паттерна, а местом принятия решения. В нём видны режимы <code>legacy-only</code>, ограниченное предложение нового пути и возврат к legacy. Переключатель должен жить на границе операции. Если флаг разбросан по внутренним вызовам, один запрос может частично пройти по старому пути и частично по новому. Тогда сравнение теряет смысл.</p>\n<p>Постепенное вытеснение не означает автоматическую безопасность. AWS описывает strangler fig как способ постепенно заменять отдельную функциональность работающего монолита. Это снижает размер изменения, но не проверяет бизнес-смысл полей и не отменяет работу с состоянием. Для каждой операции всё равно нужны owner, наблюдаемый сигнал, окно проверки и правило остановки.</p>\n<h2>Порядок действий</h2>\n<ol><li>Зафиксируйте наблюдаемый симптом и цену ошибки. Укажите, что может раздвоиться: ответ, запись, сообщение или внешний вызов.</li><li>Выберите одну операцию с узкой границей. Запишите consumer, input, response, effect и owner. Если effect неизвестен, оставьте его unknown.</li><li>Составьте таблицу valid, invalid и repeat cases. Для каждого случая укажите источник поведения: код, тест, лог, трассу или ручное подтверждение.</li><li>Оставьте legacy основным путём и создайте адаптер нового пути. Адаптер не меняет побочные эффекты, которые не входят в контракт.</li><li>Проверьте новый путь на ограниченных данных. Сравните только объявленные инварианты и отдельно отмечайте расхождение, которое требует сужения шва.</li><li>Опишите ручное решение о включении. Назовите сигнал, период наблюдения, условие остановки и человека, который принимает решение.</li><li>Подготовьте возврат маршрута к известной legacy-конфигурации. Отдельно запишите, что делать с данными и внешними эффектами, которые уже нельзя отменить.</li><li>Расширяйте границу только после разбора расхождений. Если новое поведение требует общего состояния, сначала оформите этот state boundary отдельным швом.</li></ol>\n<h2>Отрицательный путь важнее красивого ответа</h2>\n<p>Совместимость часто ломается не на успешном запросе. Старый код может округлять сумму после скидки, считать повтор безопасным или возвращать особый код при отсутствии товара. Новый сервис легко выдаёт правдоподобный <code>200</code>, но записывает другое значение. Поэтому проверка должна начинаться с отказов, тайм-аутов и повторов.</p>\n<p>У операций с состоянием есть дополнительное ограничение. Возврат маршрута не удаляет созданную запись, не отменяет платёж и не отзывает сообщение у внешнего провайдера. Для такого эффекта нужны идентификатор операции, владелец сверки и отдельное правило компенсации. Если компенсация не доказана, не называйте rollout обратимым. Выберите read-only или preview-шов либо оставьте эффект у legacy.</p>\n<p>Ограничение касается и данных сравнения. Synthetic-пример, мок или локальная база проверяют форму адаптера. Они не дают оснований заявлять сохранённое поведение реального модуля. Не выдавайте зелёный тест за результат трафика и не переносите вывод с одной популяции на другую. Если новый путь видит только простые заказы, он ещё не проверен на скидки, возвраты и повторные запросы.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Первый шов готов к ограниченному рассмотрению, если другой инженер может без устного контекста показать: один вход, один ожидаемый результат, список значимых эффектов, владельца решения, набор valid/invalid/repeat cases, источник каждого факта и известный legacy-маршрут. Для маршрута есть проверяемый возврат. Для необратимых данных отдельно названо, что возврат не покрывает.</p>\n<p>Если хотя бы один из этих пунктов неизвестен, решение не провалилось. Оно ещё не достигло границы, на которой безопасно менять путь. Сузьте операцию, соберите недостающее доказательство или оставьте legacy владельцем. Готовность здесь означает не «новый сервис написан», а «расхождение можно обнаружить, остановить и объяснить».</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-decomposing-monoliths/strangler-fig.html\" target=\"_blank\" rel=\"noopener\">AWS Prescriptive Guidance: Strangler fig pattern</a> — описание постепенной замены функциональности работающего монолита.</li><li><a href=\"https://spec.openapis.org/oas/v3.1.0.html\" target=\"_blank\" rel=\"noopener\">OpenAPI Specification 3.1.0</a> — официальный формат для описания HTTP-операций и схем.</li><li><a href=\"https://opentelemetry.io/docs/concepts/observability-primer/\" target=\"_blank\" rel=\"noopener\">OpenTelemetry: Observability primer</a> — официальное объяснение сигналов, которыми проверяют поведение системы.</li></ul>"
|
||
}
|