1. Опасность путаницы между этими режимами
На слух разница между этими тремя режимами кажется почти академической. Ну правда: старый проект, старый код, что-то меняем — какая разница, как это назвать? Но в реальной работе название задачи определяет почти всё остальное: каким будет diff, что вы обязаны доказать, какие риски перечислить, можно ли откатиться без локальной катастрофы.
Коротко напомню, что такое diff. Разница между старой и новой версией файлов — её вы читаете перед коммитом и её же смотрит ревьюер. Смешали в одном diff три типа работы — усложнили жизнь и себе, и проверяющему. А legacy-проекты мстят не сразу, а чуть позже — когда цифры в отчёте MRR вдруг начинают жить собственной духовной жизнью. CashFlow Dashboard считает подписочную выручку, возвраты и prorating: поменялось поведение расчёта после «небольшой чистки кода» — красивое название задачи никого не утешит.
Полезно держать в голове совсем простую схему:
Меняется только структура кода, а поведение должно остаться тем же → refactoring
Система постепенно оздоравливается, но живёт на прежнем стеке → modernization
Меняется стек, среда выполнения, framework или build → migration
Проблема новичков тут очень человеческая: всё плохое хочется назвать «modernization», а всё про версии — «ну это просто обновим зависимости». Увы, Gradle от добрых слов моложе не становится, а javax.* сам в jakarta.* не переедет.
2. Refactoring: структура меняется, поведение — нет
С refactoring обычно проще всего: его цель честная и скромная — сделать код понятнее, короче, чище или удобнее для тестирования, не меняя наблюдаемое поведение. Никакого нового результата для пользователя, нового стека, лечения всех хронических болезней за один подход.
В контексте CashFlow Dashboard refactoring мог бы выглядеть так: в MrrEngine длинный метод calculateMrr(), где намешаны валидация, расчёт proration, обработка скидок и сборка результата. Вынесли часть логики в приватные методы, переименовали переменные, убрали дублирование — и на тех же входных данных получили тот же результат. Это refactoring.
Короткая постановка такой задачи:
# TASK_SPEC
Mode: refactoring
Goal: упростить calculateMrr() без изменения результата
Scope: MrrEngine.java
Non-goals: новые зависимости, смена SQL, переход на новый стек
Check: characterization tests проходят без изменений # поведение сохранено
Здесь важно слово Mode. Оно сразу задаёт дисциплину. Начали менять JSON-ответ endpoint’а, конфигурацию Spring, версию framework «раз уж открыли файл» — вы вышли из refactoring.
Refactoring почти всегда локален: один файл, класс, группа тестов. Его критерий — не «код стал современным», а «внешнее поведение осталось прежним». Именно поэтому characterization tests из предыдущих лекций так важны: они дают конкретную проверку — вы переставили мебель, но не сломали дом.
3. Modernization: оздоровление без смены стека
Modernization — это оздоровление устаревшего участка без смены платформы. Здесь вы работаете уже не с одной неудобной функцией, а с проблемным куском системы. Но сервис остаётся на том же стеке, в той же среде, с теми же внешними контрактами. Вы просто уменьшаете legacy-риск по шагам.
Например, в CashFlow Dashboard вы могли обнаружить, что billing-модуль дёргает старый SDK платёжного провайдера из нескольких мест, а местами ходит в базу сырыми SQL-запросами. Фиксируете текущее поведение тестами, выделяете адаптер для провайдера, переносите часть логики в понятные границы — это modernization. Внутри почти наверняка есть refactoring-шаги, но цель шире: сделать систему безопаснее для будущих изменений.
Вот так мог бы выглядеть краткий артефакт на старте modernization:
# MODERNIZATION_NOTE
Цель: снизить риск изменений в billing-модуле
Шаг 1: зафиксировать текущее поведение тестами
Шаг 2: выделить PaymentGatewayAdapter
Шаг 3: убрать прямые SQL-вызовы из сервиса
Стек не меняем: Boot 2.7, Java 8 # это ещё не migration
Ключевая мысль здесь очень полезная: modernization может содержать много refactoring-шагов, но от этого не становится migration. Пока вы не меняете версию Java, major-версию фреймворка, build tool до новой линии и не переносите приложение в новую среду — вы на территории modernization.
И здесь тоже очень легко сорваться в хаос. Частая ошибка звучит красиво: «Давайте оздоровим модуль и сразу переведём на новый Spring Boot». На бумаге бодро. В diff — как комбайн, который едет по полю без тормозов.
4. Migration: смена среды и класс риска
Migration начинается там, где вы перестаёте менять только код и начинаете менять среду, в которой он живёт. Новая версия фреймворка, новый runtime, новая major-версия зависимости, другой build tool, новый формат конфигурации, иногда новая схема данных — переход в другое технологическое состояние.
Для нашего CashFlow Dashboard в рамках этого блока migration — вещь конкретная: первый прыжок с Spring Boot 2.7 на Spring Boot 3.x и с Java 8 на Java 21. Не весь путь до стека Commerce OS, а первый крупный переход. Даже один такой шаг приносит новый baseline Java, смену пакетов javax.* на jakarta.*, обновление частей security-конфигурации и много мелких несовместимостей, которые любят прятаться до первого запуска.
Иногда увидеть migration проще всего прямо в diff. Появились такие строки — вы уже не в refactoring:
- springBootVersion = '2.7.18' // текущая major-линия
+ springBootVersion = '3.x' // новая major-линия, это уже migration
- javaVersion = '8' // старый runtime baseline
+ javaVersion = '21' // новый runtime baseline
В migration нас интересует не «код стал красивее», а сохранилось ли ключевое поведение после перехода. Это и есть простое интуитивное значение feature parity: важные пользовательские и системные сценарии работают так, как должны, несмотря на смену платформы.
Хорошая постановка migration-задачи звучит совсем иначе, чем постановка refactoring-задачи:
# TASK_SPEC
Mode: migration
Goal: подготовить pilot-переход на Boot 3.x и Java 21
Scope: build-конфиг, security-конфиг, один read-only endpoint
Non-goals: рефакторинг BillingService, новая схема БД, новые фичи
Success: pilot slice проходит текущие проверки и ведёт себя как раньше
Обратите внимание, насколько важны здесь Scope и Non-goals. В migration легко впасть в режим «раз уж всё равно трогаем» — а это самый короткий путь к непроверяемому результату. Обновили Boot и «заодно» выделили новый интерфейс, убрали legacy SQL, нормализовали timestamps в UTC — собрали в одну коробку три разных класса риска. Такая коробка обычно открывается с хлопком.
Чтобы не путаться, полезно помнить: migration — не только upgrade фреймворка. Сюда попадают другие типы переходов:
| Тип миграции | Пример для CashFlow Dashboard |
|---|---|
| Миграция framework | Spring Boot 2.7 → Spring Boot 3.x |
| Миграция runtime | Java 8 → Java 21 |
| Миграция build tool | Gradle 7.6.4 → более новая линия |
| Миграция зависимостей | новая major-версия SDK платёжного провайдера |
| Миграция конфигурации | изменение формата или ключей настроек |
| Миграция данных | отдельный переход схемы или формата хранения |
5. Три режима на одном CashFlow Dashboard
Когда теория начинает расплываться, лучше всего вернуть её к одному и тому же проектному контексту. Одни и те же файлы CashFlow Dashboard участвуют во всех трёх режимах — в разные моменты и с разными целями. Именно поэтому сравнение по таблице здесь полезнее любого пафосного определения.
| Режим | Что меняем | Что обязано остаться | Главный артефакт | Типичный риск |
|---|---|---|---|---|
| Refactoring | структуру кода | наблюдаемое поведение | TASK_SPEC + локальные проверки | случайно поменять бизнес-логику |
| Modernization | проблемный legacy-участок по шагам | внешние контракты и бизнес-смысл | risk map, baseline, поэтапный план | scope creep и слишком широкий diff |
| Migration | стек, runtime, framework, build | feature parity на ключевых сценариях | inventory, compatibility evidence, migration plan | breaking changes и дорогой rollback |
Посмотрите, как один и тот же BillingService живёт в трёх мирах. Вынесли повторяющийся кусок логики в метод — refactoring. Построили вокруг него чистую границу, выделили адаптеры, убрали хрупкие зависимости — modernization. Перевели на новый Boot, новую Java и новую линию зависимостей — migration.
То есть вопрос не в том, какой файл вы открыли, а в том, что именно вы обещаете изменить и что обязуетесь сохранить. Это очень полезная мысль для начинающих: имена режимов описывают не место в проекте, а характер изменений. Тот же build.gradle открывают и ради маленького cleanup-комментария, и ради тяжёлой migration. Внешне файл один, инженерно это разные миры.
6. Цена смешивания всех режимов в одном diff
Вот здесь и начинается практическая боль, ради которой вообще нужна вся лекция. Смешанный diff почти всегда выглядит «продуктивно»: много движения, файлов, улучшений, кажется, что команда одним махом закроет все долги. Беда в том, что он плохо читается, плохо проверяется и ещё хуже откатывается.
Посмотрите на анти-пример постановки:
# TASK_SPEC
Goal: переписать BillingService под Boot 3.x, заодно вынести
Stripe SDK в новый интерфейс и нормализовать timestamps в UTC
Acceptance: tests pass, код стал чище
Здесь в одном абзаце сидят три разные работы. Переход на Boot 3.x — migration. Новый интерфейс вокруг Stripe SDK — modernization с refactoring-элементами. Нормализация timestamps в UTC — изменение данных и поведения, возможно, отдельная миграция данных. Упали тесты — не понять, какая часть виновата. Изменился финансовый отчёт — не доказать, что это неизбежное последствие migration, а не побочка от «заодно улучшили». Нужен rollback — вы не знаете, что откатывать.
Гораздо здоровее разложить это на независимые задачи — хотя бы на три честных набора изменений: migration pilot с чёткими non-goals; modernization вокруг SDK на старом или стабилизированном новом стеке; изменение работы со временем отдельно.
Смешивание режимов особенно опасно тем, что ломает verification. У refactoring доказательство одно: поведение прежнее. У modernization другое: риск уменьшился, границы чище. У migration третье: feature parity после смены среды. Засунули всё в одну коробку — какой набор доказательств собирать? Ответ обычно честный: «ну… tests pass». Этого недостаточно. Вот почему «маленький понятный diff» — не бюрократия, а способ отделить один тип ответственности от другого и не спорить потом в review до ночи, кто именно запустил этот великолепный снежный ком.
7. Здоровая migration-постановка
Хорошая migration-задача почти всегда звучит чуть скучнее, чем хочется: меньше героизма, больше инженерной дисциплины. Но именно поэтому она становится выполнимой. Вам нужно не вдохновить систему на перемены, а ограничить её так, чтобы переход был доказуемым.
Полезный черновик может выглядеть так:
# MIGRATION_DRAFT
Цель: перейти на Boot 3.x и Java 21 для pilot slice
Scope: build-конфиг и один read-only поток
Non-goals: рефакторинг MrrEngine, смена БД, новые API
Feature parity: JSON ответа и коды ошибок не меняются
Rollback: отдельная ветка миграции и tag baseline
Evidence before start: current-state inventory + risk map
Такой текст делает сразу несколько полезных вещей. Ограниченный scope не даст утащить полсистемы за одной строкой версии в build.gradle. Non-goals спасают от «мы ещё чуть-чуть улучшили». Rollback и evidence до старта переводят задачу из эмоции «надо обновиться» в инженерный процесс.
Ещё один важный момент: migration в нашем учебном контексте — работа с высоким риском. Мышление «сейчас Claude сам всё аккуратно подправит» тут не подходит. Нужен review-first: сначала артефакты, потом проверяемые шаги, и только потом изменения. Не можете назвать целевую границу, pilot slice, non-goals и критерий сохранения поведения — задача ещё не готова к реализации. Она пока только красиво нервничает.
8. Граница миграции в этом модуле
Это важно проговорить отдельно, потому что у слова migration неприятная привычка разрастаться до размеров вселенной. В этом блоке мы не доводим CashFlow Dashboard за один прыжок до всего стека Commerce OS. Берём первый большой переход: Boot 2.7 → Boot 3.x и Java 8 → Java 21. Это не «учебная понарошку» — наоборот, инженерно честно: уже здесь достаточно breaking changes, новой совместимости, обновлённой конфигурации и зависимости от build-инструментов. Потащите одновременно ещё и следующий большой переход — снова смешаете несколько независимых migration-волн в один стрессовый проект.
Как только режим назван, работа раскладывается естественно: сначала фиксируем Migration Current State, потом сверяем его с official docs и release notes, затем собираем COMPATIBILITY_MATRIX.md, а на этой базе пишем MIGRATION_PLAN.md для пилотного среза. Пока этого набора нет, открывать build.gradle ради самого upgrade рано.
Поэтому важнее всего здесь — назвать режим до того, как вы откроете build.gradle. Refactoring — ждёте сохранения поведения при изменении структуры. Modernization — уменьшаете legacy-риск по шагам. Migration — меняете среду и доказываете feature parity на выбранном срезе. И в этот момент старый CashFlow Dashboard перестаёт быть мистическим болотом, где «что-то надо обновить», и снова становится обычной инженерной задачей: с ясным scope, маленьким diff, понятным доказательством и без надежды на удачу как на основной инструмент разработки.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ