JavaRush /Курсы /Claude code /Strangler Fig: подмена куска legacy

Strangler Fig: подмена куска legacy

Claude code
27 уровень , 3 лекция
Открыта

1. Маленького рефакторинга иногда недостаточно

На практике довольно быстро наступает момент, когда обычного «чуть почистим метод и вынесем helper» уже недостаточно: код слишком запутан, хрупок, дорог. И именно тогда у команды появляется соблазн сказать: «А давайте просто перепишем весь mrr-engine нормально». Звучит почти героически. Но героизм в legacy заканчивается длинным diff, неясным rollback и фразой «кажется, сломались отчёты за прошлый месяц».

Если смотреть на CashFlow Dashboard трезво, mrr-engine — не учебный модуль: от него зависят цифры в отчётах, сверки, аналитика, иногда решения бизнеса. Полная перепись почти всегда плохой первый ход. Заменяем маленький срез, а не весь механизм.

Хорошо это видно в сравнении двух подходов:

Подход Что происходит Чем рискуем
Big Bang rewrite Старый модуль переписывается целиком Огромный diff, трудно сравнить поведение, rollback дорогой
Постепенная подмена Новый кусок ставится рядом со старым и обслуживает только узкий сценарий Изменение меньше, поведение легче сравнить, rollback быстрый

В нашем случае таким узким сценарием может быть, например, поиск тарифного плана для подписок, где у клиента только один активный plan. Не весь расчёт MRR, не весь refund flow, не все edge cases вселенной и соседних галактик. Но реальная часть поведения, которую можно изолировать и переключить.

И здесь очень важно не подменить цель. Я не меняю бизнес-правила, не чиню старые баги «заодно», не перевожу сервис на новый стек. Беру узкий срез и направляю его через новый путь так, чтобы снаружи система вела себя как раньше. Странности старого кода, на которые уже завязан внешний контур, сохраняем как часть совместимости. Не романтично. Зато безопасно.

2. Strangler Fig на человеческом языке

Название Strangler Fig звучит как что-то между ботаникой и названием инди-группы, но идея у него очень практичная. Растение не сносит старое дерево одним ударом топора: оно растёт рядом, обвивает старую структуру, берёт на себя всё больше функций — и только потом старая часть перестаёт быть нужна. В инженерии так же: новая реализация не выходит на сцену с фразой «всем спасибо, legacy свободен», а сначала живёт рядом и доказывает, что делает работу не хуже.

Чтобы такой подход сработал, нужны четыре вещи. Первое — маленький срез, который вы умеете сравнивать. Второе — точка переключения, одна дверь: запрос уходит либо в старый путь, либо в новый. Третье — safety baseline, чтобы доказать совпадение результата. Четвёртое — простой rollback: выключили флаг — система снова на старом пути.

Схема здесь выглядит примерно так:

flowchart TD
    A[Запрос на расчёт MRR] --> B[PlanLookupFacade]
    B -->|флаг выключен| C[LegacyPlanLookup]
    B -->|флаг включен| D[PlanLookupV2]
    C --> E[Сравнение observable output]
    D --> E
    E --> F[Запись evidence в REFACTOR_LOG.md]

Заметьте: старый код мы не прячем под ковёр — он остаётся рядом эталоном для сравнения и безопасным rollback. Новая ветка не заменяет старую «по вере». Она зарабатывает это право через evidence.

Такой срез удобно фиксировать в отдельной секции уже знакомого REFACTOR_LOG.md, рядом с шагами рефакторинга: точка переключения, флаг, parity и rollback в одном месте.

## Срез: single-plan lookup
- Точка переключения: `PlanLookupFacade`
- Новый путь: `PlanLookupV2`
- Флаг: `cashflow.mrr.v2-single-plan=false`
- Parity: сравнение old/new на фиксированных сценариях
- Rollback: вернуть флаг в `false`

3. Выбор первого среза в mrr-engine

Самая частая ошибка при знакомстве со Strangler Fig — выбрать слишком большой кусок. Кажется, раз уж делать новый путь, «пусть сразу будет полезно» — «захватим и lookup тарифа, и proration, и refunds, и расчёт по нескольким подпискам, и ещё чуть-чуть отчётности». На выходе не первый безопасный срез, а маленькая гражданская война между legacy и новой реализацией.

Хороший первый срез должен быть скучным. Да, именно скучным: понятный вход, понятный выход, небольшой радиус зависимостей, быстрый rollback. В mrr-engine таким кандидатом часто становится lookup тарифного плана для single-plan подписок. А весь refund flow — плохой первый кандидат: цепляет billing, payments, граничные даты, proration и прочие сюрпризы старых финансовых систем.

Это можно свести в небольшую таблицу:

Кандидат Подходит для первого среза Почему
Lookup тарифа для single-plan Да Узкий вход, понятный output, легко сравнивать
Весь refund flow Нет Слишком много зависимостей и business edge cases
Пересчёт MRR для downgrade/upgrade Скорее нет Высокий риск и сложная логика периодов
Формирование audit log Иногда да Небольшой side effect, если есть ясная точка входа

Перед тем как что-то переключать, удобно сначала заставить Claude Code поработать в inspect-режиме. Здесь нам не нужен «умный герой», который уже переписал полмодуля, — нужен аккуратный исследователь.

Исследуйте только путь расчёта для single-plan подписок в mrr-engine.
Код не меняйте.
Найдите точку переключения, предложите флаг включения и перечислите проверки
для сравнения старого и нового пути.
Верните ответ в формате: файлы → риски → rollback → что пока не трогать.

Хороший ответ на такой запрос покажет точку входа, затронутые файлы и опасные зависимости. Если Claude первым шагом лезет в refunds, timezone normalization и исторические миграции — это не «сильная инициатива», а сигнал, что границы задачи заданы слишком широко.

И здесь очень полезен уже знакомый Workflow Kit. agents/tester.md соберёт список сценариев для сравнения, а agents/reviewer.md проверит, что срез действительно маленький и не превратился в «ещё чуть-чуть и перепишем всё».

4. Точка переключения: фасад и флаг включения

После того как срез выбран, у вас появляется следующий вопрос: где именно переключать старый и новый путь? Если ответ звучит как «поставим по if в трёх сервисах, двух helper’ах и ещё одном контроллере», вы строите не Strangler Fig, а ловушку для будущего себя. Переключение живёт в одной точке.

Проще всего думать о фасаде как об одной двери: остальной код знает только про эту дверь, а что за ней — старый путь или новый — решает сам фасад. Это и не даёт условной логике расползтись по коду.

Сначала удобно вынести флаг включения в отдельный компонент:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class MrrFeatureFlags {
    @Value("${cashflow.mrr.v2-single-plan:false}")
    private boolean v2SinglePlan;

    public boolean useV2ForSinglePlan() {
        return v2SinglePlan; // false по умолчанию = работаем по legacy-пути
    }
}

А затем направляем вызов через фасад. Конструкторы опускаю ради краткости, чтобы пример не превращался в простыню.

import org.springframework.stereotype.Component;

@Component
public class PlanLookupFacade {
    private final LegacyPlanLookup legacy;
    private final PlanLookupV2 modern;
    private final MrrFeatureFlags flags;

    public PlanInfo find(String code, boolean singlePlan) {
        return singlePlan && flags.useV2ForSinglePlan() ? modern.find(code) : legacy.find(code);
    }
}

Что здесь хорошего? Новый путь включается только для узкого сценария — singlePlan. По умолчанию всё выключено, значит безопасное состояние — старый путь, а rollback сводится к возврату флага в false.

Если между старой и новой реализацией разные внутренние модели, может понадобиться тонкий adapter или маленький anti-corruption layer. Термина пугаться не надо: это переводчик, который не даёт странным legacy-полям просочиться в новый код и наоборот. Держите его маленьким и прозрачным. У архитекторов иногда есть суперспособность построить защитный слой толще защищаемой стены.

5. Доказательство совпадения поведения путей

Вот здесь начинается самая важная часть. Сам факт существования новой реализации ещё ничего не доказывает: даже зелёные unit-тесты ничего не гарантируют, если не сравнивают старый и новый путь на одинаковых данных. Нужен не «тест на новый код», а доказательство совпадения observable output — снаружи система выглядит одинаково на согласованном наборе сценариев.

Обычно это делается несколькими уровнями сразу. Characterization tests фиксируют текущее поведение на важных сценариях. Golden master даёт снимок результата на эталонных данных. Parity-проверка гоняет одинаковые входы через старый и новый путь и сравнивает результат. Manual check закрывает то, что пока сложно автоматизировать. Да, здесь снова появляется ваша safety baseline из предыдущих лекций — просто теперь она применяется не к одному методу, а к паре старый/новый путь.

Очень маленький пример parity-теста может выглядеть так:

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

@Test
void single_plan_lookup_keeps_same_output() {
    PlanInfo oldResult = legacy.find("basic-month");
    PlanInfo newResult = modern.find("basic-month");
    assertEquals(oldResult.getMrr(), newResult.getMrr());                // MRR совпал
    assertEquals(oldResult.getBillingPeriod(), newResult.getBillingPeriod()); // период совпал
}

Конечно, в реальном проекте у вас не один test case, а набор сценариев из CHARACTERIZATION_TESTS.md: разные тарифы, даты начала периода, состояния подписки. Принцип тот же: один вход, два пути, одно сравнение. Разошлись результаты — это не повод сказать «ну новый код просто чуть логичнее», а повод остановиться и разобраться.

Очень полезно и здесь использовать Claude Code как аккуратного помощника, а не как судью верховного разлива:

Сравните observable output старого и нового пути для single-plan lookup.
Код не меняйте.
Для каждого расхождения покажите:
- входные данные,
- старый результат,
- новый результат,
- поле, в котором есть отличие,
- предположение о причине.

Хорошая практика — фиксировать результаты в REFACTOR_LOG.md рядом с описанием среза:

## Parity для single-plan lookup
- Сценариев проверено: 200
- Расхождений: 0
- Golden master на fixtures `may24`, `jun24`: совпадает
- Rollback проверен: флаг выключается без изменения API

Обратите внимание ещё на одну тонкость. Если на известную странность старого пути опираются внешние отчёты, parity должна подтвердить именно эту странность. Исправление бизнес-поведения — отдельная задача: смешаете модернизацию с изменением логики — получите другой уровень риска.

6. Claude Code в Strangler Fig без big rewrite

В таком процессе Claude Code особенно хорош не тогда, когда ему дают команду «ну давай, современный путь, фасад, архитектура, счастье», а когда его держат в понятной рамке. Работает уже привычный вам цикл: inspect → plan → маленький diff → проверки → review. Только теперь фокус не на чистке метода, а на постепенной подмене среза. Реальный новый код включается последним — на накопленном evidence.

Если посмотреть на весь цикл на одном коротком сценарии, он выглядит так:

1. Зафиксировать срез в REFACTOR_LOG.md
2. Найти одну точку переключения
3. Добавить флаг, по умолчанию выключенный
4. Подключить новый путь рядом со старым
5. Сравнить old/new на baseline-сценариях
6. Включить новый путь только для узкого случая
7. Оставить старый путь как rollback

В CashFlow Dashboard это может выглядеть совсем буднично: parity на эталонных fixtures, включение нового пути только для single-plan подписок, старый код всё ещё рядом. Прыжка в пропасть нет. Пошло не так — выключаете флаг и идёте разбираться, а не объясняете половине команды, почему весь mrr-engine внезапно живёт по новой логике.

Именно здесь Strangler Fig перестаёт быть «умным паттерном из книжки» и становится дисциплинированной инженерной практикой. Вы не героически убиваете legacy — спокойно, почти скучно отрезаете у него маленький безопасный кусок, проверяете, что новый путь даёт тот же результат на том же срезе, и только тогда доверяете ему работу. Для legacy это лучший комплимент: модернизация становится настолько контролируемой, что перестаёт быть драмой.

1
Задача
Claude code, 27 уровень, 3 лекция
Недоступна
Feature flag и facade для узкого single-plan slice
Feature flag и facade для узкого single-plan slice
1
Задача
Claude code, 27 уровень, 3 лекция
Недоступна
Parity test между legacy и v2 lookup
Parity test между legacy и v2 lookup
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ