JavaRush /Курси /Claude code /Безпечний інкрементальний цикл рефакторингу

Безпечний інкрементальний цикл рефакторингу

Claude code
Рівень 20 , Лекція 3
Відкрита

1. Рефакторинг без циклу майже завжди розповзається

Коли людина бачить важкий метод на 120 рядків, дуже хочеться написати щось на кшталт: «Claude, зроби красиво». Він робить красиво, заодно перейменовує пів проєкту й лагодить «підозрілі місця» по сусідству. А ви дивитеся на diff так, як дивляться на меблі після переїзду: ніби все ваше, але нічого не на своєму місці. Проблема не в Claude, а у відсутності циклу керування.

Рефакторинг небезпечний не тим, що змінює логіку — добрий рефакторинг логіку не чіпає. Він небезпечний тим, що легко випадково змінити поведінку під виглядом «структурного покращення». Винесли перевірку в helper, заодно перейменували пару полів, поруч виявилося дублювання, «раз уже торкнули сервіс, почистімо й залежності». Через двадцять хвилин ви вже не в рефакторингу, а в маленькому локальному апокаліпсисі.

Саме тому безпечний refactoring майже ніколи не виглядає героїчно. Він нудний і дрібний: один маленький крок, одна вузька перевірка, один зрозумілий diff, один commit. Не кіно про геніїв. Зате працює. Інженерія любить не драму, а відтворюваність.

2. З чого складається безпечний інкрементальний цикл

Якщо прибрати все зайве, безпечний цикл refactoring виглядає дуже просто — він не потребує магії й майже не змінюється від проєкту до проєкту. Змінюються лише команди harness і назви файлів. Ритм один: маленька мета, страхувальна сітка, мінімальна зміна, точкова перевірка, diff, крок зафіксовано.

flowchart TD
    A[Маленька мета] --> B[Підтвердити страхувальну сітку]
    B --> C[Мінімальна зміна]
    C --> D[Точкова перевірка]
    D --> E[Перегляд diff]
    E --> F[Один коміт]
    F --> G{Потрібен ще крок?}
    G -- Так --> A
    G -- Ні --> H[Стоп]

Зручно тримати цей цикл ще й у табличній формі — не як бюрократію, а як короткий чек перед кожним кроком.

Етап Головне питання Що використовуємо
Маленька мета Що я зміню саме зараз? TASK_SPEC.md, залучені файли
Страхувальна сітка Чим я впіймаю поломку? harness, lightweight characterization
Мінімальна зміна Чи не розповзаються межі задачі? запит Claude з явними обмеженнями
Точкова перевірка Що зламалося одразу? точковий тест, build, lint
Перегляд diff Чи розумію я кожен рядок? git diff, reviewer-agent
Фіксація Чи можна це відкотити окремо? один коміт

Зверніть увагу на останній рядок — він ключовий. Цикл закінчується не словами «ну, мабуть, стало краще», а запитанням: чи можна цей крок відкотити окремо. Не можна — крок надто великий. Діагностика болюча, зате чесна.

3. Маленька мета: один осмислений шматок за раз

Ось тут починається вся реальна робота. Найважча частина рефакторингу — не написати код, а не дозволити собі захопити зайве. Поруч майже завжди лежить «ще одна дрібниця, яку зручно підправити». Safe loop і існує, щоб ви не лікували весь організм одним пластирем.

Візьму наш Commerce OS. У OrderService.finalizeOrder() розрісся блок перевірок, метод став гірше читатися. Хороша маленька мета: «винести початкову валідацію замовлення в приватний helper». Ідея helper-а вам уже знайома; важливо, що крок уміщується в одну точкову перевірку й один зрозумілий diff. Погана мета: «привести OrderService до ладу» — межі бездонні.

Ось типовий фрагмент до рефакторингу:

public OrderResult finalizeOrder(Order order) {
    if (order == null) {
        throw new IllegalArgumentException("Замовлення обов’язкове");
    }
    if (!order.isPaid()) {
        return OrderResult.rejected("NOT_PAID");
    }
    if (order.stock() == 0) {
        return OrderResult.rejected("OUT_OF_STOCK");
    }
    return OrderResult.finalized("Замовлення підтверджено");
}

А ось результат одного допустимого кроку:

public OrderResult finalizeOrder(Order order) {
    validateOrder(order); // метод читається простіше
    if (!order.isPaid()) {
        return OrderResult.rejected("NOT_PAID");
    }
    if (order.stock() == 0) {
        return OrderResult.rejected("OUT_OF_STOCK");
    }
    return OrderResult.finalized("Замовлення підтверджено");
}

private void validateOrder(Order order) {
    if (order == null) {
        throw new IllegalArgumentException("Замовлення обов’язкове");
    }
}

Тут важливо не лише те, що код став коротшим. Важливо, що зміну ви пояснюєте одним реченням: «Я виніс початкову валідацію в приватний helper, не змінюючи public API та решти поведінки». Потрібно три абзаци на розповідь про один крок — крок надто великий.

І ще один дуже практичний момент. Іноді Claude пропонує дивну назву helper-методу, узяту із сусіднього контексту: рефакторите finalizeOrder, а він називає helper validateRefundEligibility — це не «модель переплутала», а ознака того, що контекст змішався, і diff треба читати особливо уважно. Назва — частина сенсу. Їде сенс — слідом їдуть межі задачі.

4. Запит до Claude: не допомагати надто широко

Коли маленька мета вибрана, наступне завдання — не дати Claude перетворити крок на «покращення всього доброго проти всього поганого». У запиті потрібні три речі: вузькі межі, явні обмеження й умова зупинки. Без них модель цілком щиро почне «допомагати».

Поганий запит виглядає так:

Зроби OrderService чистішим і сучаснішим.
Якщо побачиш щось поруч, теж виправ.

Це фактично запрошення до широкого refactor. А ось запит, з яким уже можна працювати:

Рефакторимо лише один крок.

Винеси початкову валідацію з методу finalizeOrder
в приватний helper validateOrder.
Не змінюй public API.
Не змінюй статуси та причини OrderResult.
Не редагуй файли поза orders-модулем.
Після зміни запусти лише OrdersUnitTest.
Якщо тести впадуть, зупинися й поясни причину.

Тут добре все: «лише один крок», межі (не змінювати API та спостережуваний результат, не виходити за межі модуля), перевірка й умова зупинки — тести впали, отже зупинитися й пояснити.

Точні команди запуску Claude Code, назви slash-команд і деякі режими залежать від версії середовища. Порядок дій не змінюється: ви не шукаєте чарівну кнопку «безпечний рефакторинг», а будуєте запит, за якого неможливо чесно «випадково допомогти занадто широко».

Якщо хочете ще підсилити цей крок, корисно попросити Claude спочатку коротко описати план зміни, а потім уже редагувати код. Замість «зроби» ви отримуєте «ось що я збираюся зробити» — і сюрпризів у diff менше.

5. Після зміни: перевірка, diff, право на стоп

Ось тут рефакторинг перетворюється або на інженерну роботу, або знову на азартну гру. Після кожної зміни ви робите дві речі: запускаєте точкову перевірку і читаєте diff. Не одну — обидві, і в такому порядку.

Для нашого Commerce OS крок виглядає так:

./gradlew test --tests '*OrdersUnitTest'
git diff -- src/main/java/com/acmeretail/orders/OrderService.java

Спочатку ви ловите поломку швидким датчиком. Потім дивитеся, що саме змінилося: тест може бути зеленим, а diff поганим — зайві перейменування, форматувальні правки в сусідніх ділянках, пересування коду без цінності.

Дуже корисно пам’ятати просте правило: якщо після рефакторингу впав наявний тест — не змінюйте тест, а розбирайтеся, чи не змінили ви поведінку. Винятки рідкісні й очевидні. Рефакторинг проходить на старих тестах, інакше ви перепишете контракт разом із перевіркою і не помітите.

Якщо у вас у Workflow Kit уже є reviewer-agent, його зручно використовувати в локальному refactor loop як додаткову пару очей. Не суддю останньої інстанції, а помічника, який скаже: «схоже, все ок», «поведінка поїхала» або «мені бракує даних». Його контракт для такого режиму може бути дуже коротким:

name: reviewer
description: Перевіряє, що рефакторинг не змінив поведінку
tools: [Read, Bash(git diff), Bash(./gradlew test*)]
output_contract: |
  - BEHAVIOR_PRESERVED / BEHAVIOR_CHANGED / INCONCLUSIVE
  - публічні символи, яких це стосується
  - посилання на вивід тестів

Це зручно не лише для автоматизації, а й для вашої голови: ви мислите вже не «подобається / не подобається diff», а категоріями preserved, changed, inconclusive.

І ще важливіше — у вас має бути право на стоп, не моральне, а процедурне. «Claude торкнувся сусіднього файлу», «після другої спроби тести все ще падають», «diff розрісся», «я не можу пояснити кожен рядок» — ви не продовжуєте, а зупиняєте крок.

Ось коротка таблиця корисних стоп-сигналів:

Сигнал Що це зазвичай означає Що робити
Diff вийшов за межі модуля межі задачі попливли відкотити зайве, звузити крок
Зачеплено сторонні файли модель «допомогла поруч» restore / discard зайві правки
Тести все ще падають після другої спроби це вже не маленький крок rollback і діагностика
Не можете пояснити diff немає керованості не commit’ити, перечитати й звузити
З’явилася думка «заодно підправлю…» починається розповзання задачі відкрити окрему задачу

Фраза «якщо не розумію diff, не йду далі» може звучати майже образливо, але на практиці це одна з найкорисніших звичок. Claude уміє писати переконливо. git diff уміє писати чесно.

6. Один крок — один коміт

Коміт у цьому циклі — не формальність у дусі «про всяк випадок зберігся», а межа сенсу. Один смисловий крок — один коміт. Не «один файл», не «одна година роботи», не «все, що накоїли до обіду». Саме одна трансформація, яку можна окремо зрозуміти й відкотити.

Для нашого прикладу це може виглядати так:

git add src/main/java/com/acmeretail/orders/OrderService.java
git commit -m "refactor(orders): виділити helper валідації завершення"

Якщо наступним кроком ви захочете перейменувати локальну змінну або винести публікацію події — це наступний коміт, а не дописування попереднього «заодно». Тоді історія не перетворюється на кашу, а ви в будь-який момент повертаєте проєкт до стабільної точки без хірургії бензопилою.

Хороше повідомлення коміту в рефакторингу майже завжди відповідає на два запитання: що змінили і чому це безпечно. Якщо з повідомлення не видно, що крок зберіг поведінку, корисна інформація втрачена.

Якщо крок пішов не туди, не просіть Claude «полагодити поверх». Спочатку diff, потім відкат конкретних файлів через Git або повернення до checkpoint’у, і тільки потім знову. Нашарування правок поверх неясного проміжного стану — улюблений спосіб перетворити маленький рефакторинг на дуже творчу археологію.

7. Commerce OS: один безпечний крок цілком

Щоб усе не залишилося на рівні красивої теорії, давайте пройдемо один повний мікроцикл на Commerce OS. Уявіть, що ви помітили: OrderService.finalizeOrder() читається погано через початкову валідацію й inline-перевірки. Одна задача: зробити метод коротшим і зрозумілішим, не змінюючи поведінки.

Спочатку ви дивитеся в CLAUDE.md, підтверджуєте local harness: є OrdersUnitTest, за бажанням — ширший прогін. Чи вистачає unit-тестів, чи вже є 1–2 lightweight characterization checks на FINALIZED і REJECTED / OUT_OF_STOCK — є, для локального кроку достатньо. Далі запит — не «відрефактори OrderService», а саме те, що потрібно в цьому кроці:

Зроби лише один крок рефакторингу в OrderService.finalizeOrder.

Потрібно винести початкову валідацію замовлення
в приватний helper validateOrder.
Не змінюй public API, статуси OrderResult і тексти причин.
Не чіпай інші файли.
Після зміни запусти OrdersUnitTest.
Якщо тест впаде, зупинися й поясни, у чому проблема.

Claude робить правку — ви не читаєте його переможний текст як епос про порятунок проєкту, а одразу запускаєте targeted check. Тест зелений, але це лише половина історії: відкриваєте diff і читаєте весь. Саме винесення helper-методу — чудово. Заодно перейменовано eventPublisher на publisher, переставлено імпорти, переписано сусідній метод «для однаковості» — відкотіть зайве й повторіть із вужчим scope.

Далі можна попросити reviewer-agent подивитися diff у режимі refactor review. Запит може бути дуже коротким:

Перевір поточний diff як behavior-preserving refactor.
Поверни один із станів:
BEHAVIOR_PRESERVED, BEHAVIOR_CHANGED або INCONCLUSIVE.
Укажи, які публічні символи зачеплено і на які перевірки ти спираєшся.
Код не змінюй.

Якщо агент повернув BEHAVIOR_PRESERVED, а ви самі розумієте diff рядок за рядком — фіксуйте. Якщо повернув INCONCLUSIVE — не сперечайтеся з ним до ранку: зазвичай це означає, що або тестів замало, або diff вийшов за зрозумілий обсяг. Корисно дізнатися це до коміту, а не після.

Коли цей ритм перестає здаватися дріб’язковим на одному методі, його потім можна підняти на ширшу межу — між сервісом та інфраструктурою. Правило те саме: маленький крок, checks, diff, commit.

Після цього ви робите коміт. І тут важливий момент: крок закінчується. Наступний не починається автоматично, не «раз уже все відкрилося, давайте винесемо ще публікацію події»: без нового маленького плану, запиту й циклу — ні. Саме ця пауза робить процес керованим.

У такому ритмі рефакторинг перестає бути проханням «зроби красиво» і стає звичайною інженерною роботою. Claude допомагає швидше рухати маленькі зрозумілі шматки. Harness дає сигнал, коли крок зламався. Diff показує, що реально сталося. Git зберігає історію, яку не соромно читати. А ви залишаєтеся не глядачем AI-шоу, а людиною, яка все ще розуміє, що саме змінюється в проєкті й чому.

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