Компонент нельзя переписать за один релиз: старые потребители должны продолжать работать, пока новая реализация проверяется. Как организовать такую замену без одновременного переключения всех потребителей?
Используют приём ветвления через абстракцию: сначала вводят стабильный интерфейс перед существующей реализацией, затем переводят потребителей на него, после чего добавляют новую реализацию и переключают вызов на неё постепенно. Старую реализацию удаляют только после проверки новой и полного перевода потребителей.
Ключевой эффект — потребители зависят от стабильной точки доступа, а не от конкретной реализации. Это позволяет изменять внутренний механизм поэтапно, сохраняя работоспособность системы на каждом промежуточном шаге.
При крупных заменах системы часто невозможно остановить поставку функций или одномоментно обновить всех потребителей. Прямое переписывание обычно создаёт длинную ветку разработки, повышает размер изменений и откладывает обнаружение интеграционных проблем до финального переключения.
Ветвление через абстракцию возникло как практический способ выполнять такие изменения небольшими шагами. Подход близок к идее постепенной миграции: новая реализация вводится рядом со старой, а зависимость потребителей от конкретного варианта устраняется заранее.
Если потребители напрямую используют старую реализацию, её нельзя безопасно заменить частями: любой промежуточный релиз должен сохранять старый способ взаимодействия. Попытка изменить реализацию и всех потребителей одновременно увеличивает область риска и требует координированного выпуска.
Неверное решение может привести к расхождению поведения, двойной обработке операций, несовместимым ожиданиям о состоянии или невозможности быстро откатить только часть изменений. Особенно опасна миграция компонента с побочными эффектами: одинаковый запрос не должен незаметно выполняться и старым, и новым механизмом.
Сначала определяют минимальную абстракцию, описывающую нужное потребителям поведение, а не внутреннее устройство компонента. Старую реализацию подключают к этой абстракции через адаптер или делают её первой реализацией интерфейса.
Затем потребителей переводят на абстракцию небольшими группами. На этом этапе поведение системы обычно не меняется: изменяется только направление зависимости. Это отдельный полезный шаг, потому что он позволяет проверить корректность границы до появления новой реализации.
После этого создают новую реализацию, совместимую с той же абстракцией. Переключение можно выполнять по потребителям, по типам операций, по трафику или по признаку, заданному для конкретной группы пользователей. Способ переключения должен обеспечивать наблюдаемость, сравнение результатов там, где это безопасно, и быстрый возврат к старой реализации.
Старую и новую реализации нельзя безоговорочно запускать параллельно. Для операций чтения допустимо теневое сравнение результатов, если оно не создаёт побочных эффектов; для операций записи требуется отдельно продумать идемпотентность, порядок событий и владение изменяемым состоянием.
Основные компромиссы — временное усложнение кода, наличие двух реализаций и необходимость поддерживать переключатель. Если миграция затягивается, временная абстракция может стать постоянным слоем, поэтому заранее задают критерии завершения и удаляют старый путь после стабилизации новой реализации.
Платформа расчёта доставки использовала старый компонент с большим количеством специальных правил. Его нельзя было заменить за один релиз: разные потребители выпускались независимыми командами, а ошибка в расчёте влияла на стоимость заказа.
Рассматривались три варианта. Полная замена за один выпуск была бы простой по структуре, но требовала синхронного обновления и несла высокий риск. Параллельный запуск старой и новой логики для всех заказов давал возможность сравнения, но мог удвоить побочные действия и усложнить разбор расхождений. Постепенное переключение через стабильную абстракцию требовало временного кода, зато ограничивало область изменений.
Выбрали третий вариант. Сначала все потребители перешли на единый интерфейс, затем новая реализация включалась для отдельных категорий доставки с метриками расхождений и возможностью быстрого возврата. После устранения различий старый компонент и переключатель удалили.
Результат — миграция прошла без единого массового релиза. Цена решения состояла в поддержке двух реализаций в течение переходного периода и в необходимости явно контролировать различия между ними.
1. Достаточно ли создать интерфейс, чтобы миграция считалась выполненной?
Нет. Если потребители продолжают зависеть от старой реализации, интерфейс существует только формально. Сначала нужно перевести зависимости потребителей на абстракцию, иначе новая реализация не создаёт реальной свободы переключения.
Кроме того, абстракция должна отражать потребности потребителей и скрывать детали конкретной реализации. Интерфейс, повторяющий все внутренние методы старого компонента, лишь переносит старую связанность на новый уровень.
2. Можно ли безопасно сравнивать старую и новую реализацию на каждом запросе?
Не всегда. Для чистых операций чтения можно выполнить новую реализацию в теневом режиме и сопоставить результаты, не используя их для ответа. Для записи такой подход может повторно отправить событие, списать деньги или изменить состояние.
Для операций с побочными эффектами применяют отдельную стратегию: сравнивают вычисления до записи, используют идемпотентные операции, тестовые копии данных или ограниченное включение новой реализации. Без этого параллельный запуск может создать более серьёзную проблему, чем сама миграция.
3. Чем этот подход отличается от обратной совместимости контракта?
Обратная совместимость обычно позволяет старым и новым потребителям временно использовать изменяемый внешний контракт. Ветвление через абстракцию решает другую задачу: оно отделяет потребителей от заменяемой внутренней реализации.
Оба подхода могут применяться вместе, но не заменяют друг друга. Даже совместимый контракт не помогает, если потребители напрямую зависят от деталей старого алгоритма, состояния или конкретного компонента.