JavaRush /Курсы /Claude code /Safe incremental refactoring loop

Safe incremental refactoring loop

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

1. Refactor без цикла почти всегда расползается

Когда человек видит тяжёлый метод на 120 строк, очень хочется написать что-то вроде: «Claude, сделай красиво». Он делает красиво, попутно переименовывает полпроекта и чинит «подозрительные места» по соседству. А вы смотрите на diff так, как смотрят на мебель после переезда: вроде всё ваше, но ничего не на своём месте. Проблема не в Claude, а в отсутствии цикла управления.

Refactor опасен не тем, что меняет логику — хороший refactor логику не трогает. Он опасен тем, что легко случайно изменить поведение под видом «структурного улучшения». Вынесли проверку в helper, заодно переименовали пару полей, рядом обнаружилось дублирование, «раз уж тронули сервис, подчистим и зависимости». Через двадцать минут вы уже не в refactor, а в маленьком локальном апокалипсисе.

Именно поэтому безопасный refactoring почти никогда не выглядит героически. Он скучный и дробный: один маленький шаг, одна узкая проверка, один понятный diff, один commit. Не кино про гениев. Зато работает. Инженерия любит не драму, а воспроизводимость.

2. Из чего состоит safe incremental loop

Если убрать всё лишнее, безопасный цикл refactoring выглядит очень просто — он не требует магии и почти не меняется от проекта к проекту. Меняются только команды harness и имена файлов. Ритм один: маленькая цель, safety net, минимальное изменение, точечная проверка, diff, шаг зафиксирован.

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

Удобно держать этот цикл ещё и в табличной форме — не как бюрократию, а как короткий чек перед каждым шагом.

Этап Главный вопрос Что используем
Маленькая цель Что меняю ровно сейчас? TASK_SPEC.md, затронутые файлы
Safety net Чем я поймаю поломку? harness, lightweight characterization
Минимальное изменение Не расползаются ли границы задачи? запрос Claude с явными ограничениями
Точечная проверка Что сломалось сразу? точечный тест, build, lint
Просмотр diff Понимаю ли я каждую строку? git diff, reviewer-agent
Фиксация Можно ли это откатить отдельно? один commit

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

3. Маленькая цель: один осмысленный кусок за раз

Вот здесь начинается вся реальная работа. Самая трудная часть refactoring — не написать код, а не позволить себе захватить лишнее. Рядом почти всегда лежит «ещё одна мелочь, которую удобно поправить». Safe loop и существует, чтобы вы не лечили весь организм одним пластырем.

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

Вот типичный фрагмент до refactor:

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 и observable result, не выходить за модуль), проверка и условие остановки — тесты упали, значит остановиться и объяснить.

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

Если хотите ещё усилить этот шаг, полезно попросить Claude сначала кратко описать план изменения, а потом уже редактировать код. Вместо «сделай» вы получаете «вот что я собираюсь сделать» — и сюрпризов в diff меньше.

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

Вот тут refactor превращается либо в инженерную работу, либо обратно в азартную игру. После каждого изменения делаете две вещи: запускаете точечную проверку и читаете diff. Не одну — обе, и в таком порядке.

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

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

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

Очень полезно помнить простое правило: упал после refactor существующий тест — не менять тест, а разбираться, не изменили ли вы поведение. Исключения редки и очевидны. Refactor проходит на старых тестах, иначе перепишете контракт вместе с проверкой и не заметите.

Если у вас в 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 вышел за модуль границы задачи поплыли откатить лишнее, сузить шаг
Тронуты unrelated files модель «помогла рядом» restore / discard лишние правки
Тесты всё ещё падают после второй попытки это уже не маленький шаг rollback и диагностика
Не можете объяснить diff нет управляемости не commit’ить, перечитать и сузить
Появилась мысль «заодно поправлю…» начинается расползание задачи открыть отдельную задачу

Фраза «если не понимаю diff, не иду дальше» может звучать почти обидно, но на практике это одна из самых полезных привычек. Claude умеет писать убедительно. git diff умеет писать честно.

6. Один шаг — один commit

Коммит в этом цикле — не формальность в духе «на всякий случай сохранился», а граница смысла. Один смысловой шаг — один commit. Не «один файл», не «один час работы», не «всё, что наделали до обеда». Именно одна трансформация, которую можно отдельно понять и откатить.

Для нашего примера это может выглядеть так:

git add src/main/java/com/acmeretail/orders/OrderService.java
git commit -m "refactor(orders): extract finalization validation helper"

Если следующим шагом вы захотите переименовать локальную переменную или вынести публикацию события — это следующий commit, а не дописывание предыдущего «заодно». Тогда история не превращается в кашу, а вы в любой момент возвращаете проект к стабильной точке без хирургии бензопилой.

Хорошее сообщение коммита в refactor почти всегда отвечает на два вопроса: что изменили и почему это безопасно. Если из сообщения не видно, что шаг был behavior-preserving, полезная информация потеряна.

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

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», а именно то, что нужно в этом шаге:

Сделай только один refactor-шаг в 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.

После этого вы делаете commit. И тут важный момент: шаг заканчивается. Следующий не начинается автоматически, не «раз уж всё открылось, давайте вынесем ещё публикацию события»: без нового маленького плана, запроса и цикла — нет. Именно эта пауза делает процесс управляемым.

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

1
Задача
Claude code, 20 уровень, 3 лекция
Недоступна
Один refactor-step — extract helper без расширения scope
Один refactor-step — extract helper без расширения scope
1
Задача
Claude code, 20 уровень, 3 лекция
Недоступна
Один refactor-step — локальный rename private symbol
Один refactor-step — локальный rename private symbol
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ