JavaRush /Курсы /Claude code /Refactoring и behavior preservation

Refactoring и behavior preservation

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

1. «Почистить код» — это ещё не рефакторинг

Слово refactoring новички часто понимают слишком житейски: «навести порядок», «сделать почище», «убрать ужас». Звучит мило, но в таком виде это опасное определение. Код станет аккуратнее и «умнее», а бизнес-поведение уедет — и это уже не рефакторинг, а другая работа под видом уборки.

Если говорить строго, то refactoring — это structural change without behavior change. Изменение структуры без изменения видимого снаружи поведения. Поэтому refactor идёт через тот же issue-to-PR loop, что feature и bugfix, но с дополнительным критерием — behavior preserved.

Refactoring = меняем форму кода, но не меняем его внешний контракт.

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

Небольшой пример из Commerce OS. Допустим, в OrderService длинный метод: проверка заказа, потом логика. Вынесли проверку в приватный метод, вход и выход не тронули — рефакторинг.

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("Заказ обязателен");
    }
}

Здесь код стал чуть лучше организован, но внешний смысл метода не изменился: контроллер, тесты, интеграции видят тот же результат.

А если в процессе «улучшения» вы поменяли текст ошибки, порядок бизнес-проверок, HTTP-статус, схему ответа или момент отправки события — это уже не «просто уборка», а изменение поведения. Код стал элегантнее, а checkout перестал работать? Не архитектурное озарение, а обычный баг. В красивой упаковке.

2. У кода нет табличек «внутри» и «снаружи»

«Поведение снаружи не меняется» звучит просто. Но где граница? Для одного модуля внешний мир — HTTP-клиент. Для другого — соседний сервис, база, очередь событий, письмо, webhooks, админка. Refactoring начинается с вопроса: что здесь считается внешне наблюдаемым поведением?

Для Commerce OS мыслите так:

Обычно относится к внешне наблюдаемому поведению Обычно относится к внутренней структуре
HTTP status, JSON-ответ, публичный контракт сервиса, записи в БД, отправленные события, текст ошибки, который видит клиент, порядок наблюдаемых побочных эффектов имена локальных переменных, выделение приватного helper-метода, перестановка внутренних строк без изменения результата, разбиение длинного метода, перенос логики между private-методами

Ключевое слово здесь — обычно. Потому что один и тот же код в разных системах может находиться на разной глубине контракта. Например, переименование приватной переменной почти никогда не влияет на поведение. А вот переименование публичного метода, который использует внешний модуль, уже может быть breaking change. То есть не refactoring, а изменение контракта.

Есть и более коварный случай: код визуально почти не меняется, а поведение уже уехало. Посмотрите на такой фрагмент:

if (!order.isPaid()) {
    return OrderResult.rejected("NOT_PAID");
}
if (order.stock() == 0) {
    return OrderResult.rejected("OUT_OF_STOCK");
}

Если вы просто поменяете эти проверки местами, код останется «почти таким же», но для заказа, который и не оплачен, и попадает в OUT_OF_STOCK, система вернёт другую причину отказа. Снаружи видно? Да. Значит, изменение behavior.

Именно поэтому фраза «я же только чуть-чуть переставил условия» ревьюера не успокаивает. Порядок проверок и событий, момент записи в базу, текст ошибок, форма ответа — не косметика, а поведение системы. Если после правки клиент, соседний модуль или тест может законно спросить «почему ответ теперь другой?» — вы вышли за границы refactor-задачи.

3. Граница: refactor vs bugfix, feature, rewrite

На практике главная путаница возникает не внутри самого refactoring, а на границе с соседними типами работы. Человек искренне говорит: «Я просто почистил сервис» — а на деле исправлен дефект, добавлен сценарий, обновлена зависимость. Для работы с Claude эту границу нужно делать буквальной, а не интуитивной.

Удобнее всего сравнить категории в таблице:

Что вы делаете Меняется ли внешнее поведение Это что? Пример в Commerce OS
Выделяете приватный helper, упрощаете условие, убираете дублирование Нет Refactoring Вынесли проверку заказа из finalizeOrder() в validateOrder()
Исправляете неправильный результат Да, и это специально Bugfix Исправили дублирование refund-запросов
Добавляете новый сценарий или поле Да Feature Добавили флаг estimatedDelivery в ответ API
Переписываете модуль почти с нуля Формально может быть «нет», но риск огромный Rewrite Полностью переписали OrderService на новую внутреннюю модель
Постепенно улучшаете старый модуль с safety net и границами Обычно стараетесь сохранить behavior, но масштаб шире Modernization Чистите связность и boundaries старого подсистемного блока
Обновляете framework, runtime, зависимости, конфиг-модель Часто меняется окружение и контракт выполнения Migration Переходите на новую major-версию Spring Boot

Самый частый самообман выглядит так: «Заодно исправил старый баг, пока делал refactor». Нет, не «заодно». Баг исправлен — поведение изменилось. Это bugfix.

Вторая популярная ловушка: «Ничего не менял для пользователя, просто переписал модуль нормально». Модуль переписан целиком — риск уже другой. Без плотной safety net и маленьких шагов это rewrite с элементами надежды. Надежда — неплохое чувство, но плохая стратегия code review.

Есть ещё коварная разновидность — «рефакторинг с миграцией внутри». Например, вы выносите publisher в отдельный класс, а заодно обновляете библиотеку событий, «она и так старая». Всё, две задачи в одном PR: reviewer оценивает structural improvement и dependency change разом. Гарантированная путаница.

Поэтому инженерное правило здесь очень простое:

Если изменение должно улучшить поведение — это не refactor.
Если изменение должно изменить среду исполнения — это не refactor.
Если изменение должно только улучшить структуру — тогда да, это кандидат на refactoring.

4. Хороший refactor решает structural problem, а не улучшает поведение

Чтобы всё это не осталось на уровне красивых определений, полезно посмотреть на изменения, которые действительно попадают в категорию refactoring. Мерка здесь не «стало короче», а «стало понятнее, легче тестировать, но контракт снаружи прежний».

Чаще всего сюда попадают извлечение функции, переименование внутренних сущностей, уменьшение вложенности условий, устранение дублирования, разделение большой функции на несколько маленьких, вынос side effects ближе к их владельцу, удаление действительно мёртвого кода после проверки ссылок и тестов.

Например, вот такой метод можно упростить без изменения поведения:

private boolean canApproveRefund(Order order) {
    if (order != null) {
        if (order.isPaid()) {
            return !order.isArchived();
        }
    }
    return false;
}

После упрощения:

private boolean canApproveRefund(Order order) {
    return order != null
            && order.isPaid()
            && !order.isArchived();
}

Если для всех входных значений результат остался тем же — это нормальный refactor.

Другой хороший кандидат — уменьшение дублирования. Одну приватную проверку из трёх мест выносите в один helper. Главное — не начать «слегка улучшать правила»: тогда вы меняете логику, а не выносите дублирование.

Отдельно стоит сказать про удаление мёртвого кода. Оно выглядит безобидно, но тут новички особенно любят попасть в ловушку. Код кажется неиспользуемым — пока вы не открыли references, call hierarchy и не глянули на тесты. Удалять «мёртвые» ветки без опоры на harness и code intelligence — не аккуратность, а лотерея с неприятным призом.

То есть хороший refactor почти всегда отвечает на простой вопрос: какую structural problem он решает? «Уменьшает дублирование», «изолирует ответственность», «делает метод тестопригоднее» — верный путь. «Теперь пользователь увидит лучшее поведение» — не refactor.

5. Не давайте Claude творческого разгона

Самая частая ошибка при работе с Claude на refactor-задаче звучит так: «Сделай этот файл нормальным». Модель действительно старается помочь — и, как это часто бывает с очень старательными помощниками, помогает слишком широко: лезет в несвязанные файлы, «на всякий случай» правит логику. Поэтому хороший refactor с Claude начинается не с редактирования, а с классификации возможностей.

Плохой запрос выглядит так:

Сделай OrderService чище и современнее.

У такого запроса нет ни границ, ни списка «чего не делать», ни критерия behavior preserved. Для AI это почти приглашение к творчеству.

Гораздо лучше сначала попросить Claude ничего не менять — только найти кандидатов на refactor и разложить их по риску и пользе:

Найди в src/main/java/com/acmeretail/orders/OrderService.java
локальные возможности для refactor.

Ничего не меняй.

Для каждого предложения укажи:
1. тип изменения;
2. зачем это полезно;
3. риск;
4. какие файлы затронутся;
5. что должно остаться неизменным снаружи.

Не предлагай feature changes, bug fixes, dependency upgrades
и framework changes.

Такой запрос делает две важные вещи: отделяет анализ от реализации и прямо запрещает соседние категории. Claude в узком коридоре, вы — со списком кандидатур, из которого выбираете одну structural problem.

Когда конкретная цель уже выбрана, второй запрос тоже должен быть узким:

Выполни только один refactor-шаг:
вынеси локальную валидацию заказа из finalizeOrder()
в private helper validateOrder().

Ограничения:
- не менять public API;
- не менять тексты ошибок;
- не менять порядок бизнес-проверок;
- не добавлять зависимости;
- после изменений запустить существующие тесты orders.

Если увидишь bugfix opportunity, остановись и опиши её отдельно.

Вот это уже хорошая работа с Claude: вы не просите «улучшить всё», а меняете ровно один structural aspect и сразу ставите guardrails. Если Workflow Kit держит эти ограничения в CLAUDE.md, Claude ещё реже импровизирует на тему «перепишем-ка заодно соседний модуль».

Но даже у аккуратного запроса остаётся очень приземлённый вопрос: harness вообще увидит, если вы случайно сдвинули именно этот observable result? Если нет — перед первой правкой зафиксируйте пару узких checks на текущее поведение.

И здесь очень важно помнить старую формулу курса: Claude edits are proposals expressed as diffs. Даже идеальный prompt не отменяет чтение diff и прогон harness. Claude полезен не тем, что «знает, как красиво», а тем, что быстро предлагает маленькую трансформацию в заданных рамках.

6. Один refactor PR — одна structural problem

Даже если вы прекрасно понимаете теорию, PR всё равно норовит распухнуть. Claude замечает соседний дубликат, старый TODO, кривое имя метода — и радостно предлагает «заодно» всё подчистить. Здесь и решается: профессиональный refactor или классика про «слегка убрались, а половина сервиса переехала».

Представьте типичный случай в Commerce OS. Вы пришли в OrderService изолировать валидацию. Claude замечает в refund-ветке спорную бизнес-логику. Соблазн: «Раз уж я здесь — поправлю сразу». Нет. Именно тут PR теряет чистоту.

Правильный TASK_SPEC.md звучит примерно так:

Goal: simplify validation flow in OrderService.finalizeOrder.
Non-goals: change refund rules, response texts, DB writes.
Acceptance: existing order tests pass without changes.

Видите, что здесь важно? Прямо сказано и «что делаем», и «что не трогаем». В refactor-PR это критично: такие PR любят притворяться безопасными, пока в них тихо не прячется новая бизнес-логика.

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

В инженерной культуре есть очень зрелое действие, которое новичкам сначала кажется скучным: вовремя остановиться. Поняли во время refactor, что поведение и правда надо менять, — это не провал, а новая задача. Профессионализм не в том, чтобы незаметно впихнуть её в текущий PR, а в том, чтобы честно вынести отдельно.

Именно так refactoring перестаёт быть романтическим «сделаем красиво» и становится нормальной инженерной практикой: узкой, проверяемой, ревьюемой и безопасной для проекта. А это куда полезнее любой красоты ради красоты.

1
Задача
Claude code, 20 уровень, 1 лекция
Недоступна
Переименование private-метода без изменения public API
Переименование private-метода без изменения public API
1
Задача
Claude code, 20 уровень, 1 лекция
Недоступна
Классификация предложенных изменений: refactor или нет
Классификация предложенных изменений: refactor или нет
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ