← Заметки

Рефакторинг с ИИ-агентом: сохранение поведения и проверяемые изменения

На этой странице

Рефакторинг с ИИ-агентом начинается с определения сохраняемого поведения и области изменений, затем проверяет результат выполнением и diff. Более короткая функция или сообщение «тесты прошли» не означают завершения. Проверьте сохранение значений, исключений и внешних эффектов и отсутствие ослабления самой проверки.

Руководство не зависит от инструмента и адресовано знакомым с Git и тестами. Конкретные интерфейс и модель не предполагаются. Запросы ниже — написанные примеры для копирования и адаптации, не диалоги продукта и не измерения производительности. Результаты выполнения Java выделены отдельно.

Что включить в первый запрос?

Мартин Фаулер определяет рефакторинг как изменение внутренней структуры с сохранением наблюдаемого снаружи поведения. Если нужна новая политика или исправление ошибки, отделите их для оценки разницы до и после.

Передайте агенту файлы, вызывающий код, связанные тесты, соглашения репозитория и реальные команды проверки. Вместо безусловного чтения всего репозитория начните с нужных путей. Описание текущего поведения и оснований без правок сначала снижает риск расширения изменений из-за непонимания.

Политика доставки ниже — требования для иллюстрации, не реальная политика или отчёт об инциденте.

Условие Сохраняемое поведение
Отрицательная сумма заказа Выбросить IllegalArgumentException("subtotal")
Участник или сумма не менее 50,000 KRW Базовая доставка 0 KRW
Иначе Базовая доставка 3,000 KRW
Островная или удалённая доставка Добавить 3,000 KRW независимо от бесплатной базы

Первый запрос можно написать так. Предположим, полный код далее сохранён как ShippingRefactorDemo.java. Для практики сначала скопируйте тело before() в after(). Полный пример ниже включает завершённый рефакторинг для сравнения.

Прочитай before(), after() и main() в ShippingRefactorDemo.java и только проанализируй.
Пока не изменяй файлы.

Цель: сократить вложенные ветви after(), сохранив расчёт доставки before().
Входные суммы в KRW, используется long.
Проверь тип и сообщение исключения отрицательной суммы, границу 50,000 KRW,
сочетания бесплатной базовой доставки и доплаты удалённого района.

Укажи кодовые основания текущего поведения, уже охваченные тесты,
дополнительные граничные случаи и минимальное изменение только after().
Не меняй неясные правила на основе догадок.

Здесь нужен результат по каждому условию и места изменений, а не абстрактный «план качества». Например, «участники всегда платят 0 KRW» противоречит доплате удалённого района; исправьте понимание до реализации.

Сделайте небольшое изменение и сравните поведение

Пример разделяет расчёт базовой доставки и доплаты из дублирующихся веток. Сохраняются входы, выходы и исключения без интерфейса стратегии и конфигурационного слоя. Небольшой diff помогает проверить изменение структуры без политики. Рекомендации Google

При отрицательной сумме выбросить то же исключение, вычислить базу по участию и порогу, добавить удалённую доплату и вернуть итог

Схема показывает расчёт after(), не внутренности ИИ-инструмента. before() и after() иллюстративны; main() проверяет явные ожидания и сравнивает результаты. Работает на JDK 21 или новее без внешних зависимостей.

public class ShippingRefactorDemo {
    static final long FREE_THRESHOLD = 50_000;
    record Case(long subtotal, boolean member, boolean remote, long expected) {}

    static long before(long subtotal, boolean member, boolean remote) {
        if (subtotal < 0) throw new IllegalArgumentException("subtotal");
        if (member) {
            if (remote) return 3_000;
            return 0;
        }
        if (subtotal >= 50_000) {
            if (remote) return 3_000;
            return 0;
        }
        if (remote) return 6_000;
        return 3_000;
    }

    static long after(long subtotal, boolean member, boolean remote) {
        if (subtotal < 0) throw new IllegalArgumentException("subtotal");
        long base = (member || subtotal >= FREE_THRESHOLD) ? 0 : 3_000;
        long surcharge = remote ? 3_000 : 0;
        return base + surcharge;
    }

    static void equal(long actual, long expected) {
        if (actual != expected) {
            throw new AssertionError("expected=" + expected + ", actual=" + actual);
        }
    }

    public static void main(String[] args) {
        Case[] examples = {
            new Case(0, false, false, 3_000),
            new Case(49_999, false, false, 3_000),
            new Case(50_000, false, false, 0),
            new Case(0, true, false, 0),
            new Case(49_999, false, true, 6_000),
            new Case(50_000, false, true, 3_000),
            new Case(0, true, true, 3_000),
            new Case(Long.MAX_VALUE, false, true, 3_000)
        };
        for (Case c : examples) {
            equal(before(c.subtotal(), c.member(), c.remote()), c.expected());
            equal(after(c.subtotal(), c.member(), c.remote()), c.expected());
        }
        System.out.println("examples=" + examples.length);

        int comparisons = 0;
        long[] amounts = {0, 1, 49_999, 50_000, 50_001, Long.MAX_VALUE};
        for (long subtotal : amounts) {
            for (boolean member : new boolean[] {false, true}) {
                for (boolean remote : new boolean[] {false, true}) {
                    equal(after(subtotal, member, remote), before(subtotal, member, remote));
                    comparisons++;
                }
            }
        }
        System.out.println("comparisons=" + comparisons);

        int rejected = 0;
        for (boolean refactored : new boolean[] {false, true}) {
            try {
                if (refactored) after(-1, false, false);
                else before(-1, false, false);
                throw new AssertionError("Negative amount was accepted");
            } catch (IllegalArgumentException e) {
                if (!"subtotal".equals(e.getMessage())) throw new AssertionError(e);
                rejected++;
            }
        }
        System.out.println("negative-inputs=" + rejected);
    }
}
javac --release 21 ShippingRefactorDemo.java
java ShippingRefactorDemo

Вывод получен на Zulu JDK 21.0.1+12-LTS. Начальная версия с одинаковым телом after() и before() и завершённая ниже дали этот вывод.

examples=8
comparisons=24
negative-inputs=2

Вместо Java assert, отключаемого в зависимости от -ea, код напрямую бросает AssertionError при разнице. Числа относятся к заданным случаям и комбинациям, а не покрытию всей производственной базы.

Запрашивая реализацию, свяжите проверенные условия с областью правок и командами.

Выполни рефакторинг только after(), сохранив установленный контракт доставки.
Не меняй before(), main(), ожидаемые значения и FREE_THRESHOLD.
Сохрани публичные входы и выходы, типы и сообщения исключений; не добавляй зависимости и слои.
Если нужны изменения вне файла, объясни причины, не применяя их.

Выполни javac --release 21 ShippingRefactorDemo.java и java ShippingRefactorDemo.
Предоставь реальные команды, коды выхода, наблюдаемый вывод и diff.
При ошибке сузь условие и причину и исправь; не удаляй проверки и не подгоняй ожидания.

Если среда агента не редактирует файлы или не запускает команды, примените патч и выполните команды сами. «Не менять» в запросе задаёт область, не реальные права инструмента и файловые ограничения.

Равны ли успешные тесты сохранению поведения?

Проверяются 8 явных ожиданий, 24 комбинации до и после и исключения обоих методов на отрицательных входах. Это не доказательство эквивалентности всех long. Одна эталонная before() может сохранять старые ошибки; нужны независимые ожидания и бизнес-требования. Найдя дефект, отдельно решите, исправлять ли его и какое поведение изменится.

Контрпример проверит чувствительность тестов. Замена subtotal >= FREE_THRESHOLD в after() на subtotal > FREE_THRESHOLD исключит ровно 50,000 KRW для неучастника из бесплатной базы. Проверка ожидания должна упасть.

Временная копия с этой единственной неверной границей проверенно упала с AssertionError: expected=0, actual=3000. Это подтверждает обнаружение конкретной ошибки, не всех неверных изменений. В завершённом коде мутации нет.

В проекте кроме значений проверяйте исключения, запросы и транзакции БД, число и порядок внешних вызовов, сериализацию и освобождение ресурсов. Успех чистой функции не проверяет БД и HTTP. Для производительности нужны отдельные измерения с фиксированными входами и средой.

Начните финальную проверку с diff, а не объяснения

До описания агента просмотрите diff и результаты. Ищите лишние файлы, удалённые или пропущенные тесты, проглоченные исключения и несвязанные зависимости. git diff --check проверяет пробелы и подобное, не бизнес-поведение.

В отдельный контекст проверки передайте исходные требования, код до и после, diff и выполнение вместо исходного заявления об успехе. Рецензия тоже ошибается; сверяйте замечания с кодом и требованиями. Документация GitHub об ограничениях также предупреждает о неточности кода и замечаний.

Проверь изменение без правок.
Вход: требования доставки, код до и после, diff, команды тестов и реальные результаты.

Найди контрпримеры изменения значений, исключений, границы 50,000 KRW и удалённых доплат.
Проверь выход за область и ослабление тестов.
Для замечания укажи место, пример входа, наблюдаемую разницу и основание.
Не называй невыполненные проверки выполненными.

В краткой передаче опишите структуру, сохранённое поведение, выполненные проверки и остаток. При провале уточните вход и ожидание и сузьте исправление; выросшие изменения разделите. Размер, при котором проверяющий объяснит сохранение поведения, остаётся полезной единицей завершения с ИИ-агентом.

Обсуждение

Посмотреть на GitHub ↗