JavaRush /Курсы /Claude code /Refactoring, modernization, migration

Refactoring, modernization, migration

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

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.7Spring Boot 3.x
Миграция runtime Java 8Java 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.7Boot 3.x и Java 8Java 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, понятным доказательством и без надежды на удачу как на основной инструмент разработки.

1
Задача
Claude code, 28 уровень, 0 лекция
Недоступна
Терминальная фиксация признаков migration-задачи
Терминальная фиксация признаков migration-задачи
1
Задача
Claude code, 28 уровень, 0 лекция
Недоступна
Решение о режиме работы: refactoring vs modernization vs migration
Решение о режиме работы: refactoring vs modernization vs migration
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ