JavaRush /Курси /Claude code /Рефакторинг, модернізація, міграція

Рефакторинг, модернізація, міграція

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, а перший великий перехід. Навіть один такий крок приносить нову базову лінію Java, зміну пакетів javax.* на jakarta.*, оновлення частин security-конфігурації і багато дрібних несумісностей, які люблять ховатися до першого запуску.

Іноді побачити migration найпростіше прямо в diff. Зʼявилися такі рядки — ви вже не в refactoring:

- springBootVersion = '2.7.18'   // поточна major-лінія
+ springBootVersion = '3.x'      // нова major-лінія, це вже migration

- javaVersion = '8'              // старий baseline runtime
+ javaVersion = '21'             // новий baseline runtime

У migration нас цікавить не «код став красивішим», а чи збереглася ключова поведінка після переходу. Це й є просте інтуїтивне значення feature parity: важливі користувацькі та системні сценарії працюють так, як мають, попри зміну платформи.

Гарна постановка migration-задачі звучить зовсім інакше, ніж постановка refactoring-задачі:

# TASK_SPEC
Mode: migration
Goal: підготувати пілотний перехід на 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 і надто широкий diff
Migration стек, runtime, framework, build feature parity на ключових сценаріях інвентаризація, докази сумісності, 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, зрозумілим доказом і без надії на удачу як на основний інструмент розробки.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ