Избегание рефакторинга.
ИИ-ассистенты почти никогда не перестраивают существующий код: они навешивают на то, что уже есть, новые ветки, параметры-флаги и обёртки, поэтому функции и условные операторы всё растут, а рефакторинг — переработка, поддерживающая здоровье кодовой базы, — незаметно исчезает.
##Признаки и симптомы
Ревьюер видит дифф, почти целиком состоящий из добавлений к уже существующему коду. Модель решила задачу, надстроив ещё одну ветку, флаг или слой над функцией, на которую ей указали, вместо того чтобы перестроить эту функцию так, чтобы изменение в неё вписалось. Характерные признаки:
- Диффы из одних добавлений: ни одной перемещённой, переименованной или удалённой строки; затронутая функция только удлиняется.
- Параметры-флаги:
isWholesale,skipValidation,legacy = false, дописанные к существующей сигнатуре, — каждый переключает поведение где-то в глубине тела. - Разрастание ветвлений: каждое новое требование превращается в ещё один
else ifилиcaseв той же функции вместо таблицы поиска, стратегии или полиморфного вызова. - Обёртка сверху:
processOrderV2вызываетprocessOrderи подправляет его результат, вместо того чтобы изменить его; старый и новый пути живут бок о бок. - Метрики растут в одном месте: когнитивная сложность, длина и число параметров одних и тех же функций растут от коммита к коммиту — горячая точка увеличивается.
- Мёртвые флаги не удаляются: параметры, которые все вызывающие теперь передают одинаково, оставлены «на всякий случай».
// Каждый запрос добавлял параметр и ветку; перестройки не было ни разу.
function priceOrder(
items: Item[],
discount?: number,
isWholesale = false,
applyVat = true,
couponCode?: string,
) {
let total = items.reduce((s, i) => s + i.price * i.qty, 0);
if (discount) total = total * (1 - discount);
if (isWholesale) {
if (total > 1000) total = total * 0.9;
else if (total > 500) total = total * 0.95;
}
if (couponCode === "SPRING") total = total - 10;
else if (couponCode === "VIP") total = total * 0.85;
if (applyVat) total = total * 1.2;
return total;
}
Каждый запрос добавил один параметр и одну ветку, и каждый дифф затрагивал только конец функции. Ничего не продублировано, поэтому детектор клонов молчит; запах проявляется в росте сложности и размера.
##Причины проблемы
Почему модели это порождают
- Аддитивное смещение предсказания следующего токена. LLM дополняет тот промпт, который перед ней. Дописать ветку или параметр в месте изменения — локально самое вероятное и наименее рискованное продолжение; перестройка функции требует удерживать в рабочем контексте весь модуль и его вызывающий код и предсказать глобально согласованную правку, что сложнее и не вознаграждается непосредственным промптом.
- Скудный контекст репозитория. Кодовые ассистенты редко загружают весь вызывающий код и все тесты. Не видя, кто зависит от функции, модель не может понять, безопасно ли её перестраивать, поэтому оставляет структуру как есть и добавляет
ifтам, где видит. OX Security описывает ИИ как «армию талантливых, но неопытных младших разработчиков» — продуктивных локально, но не следящих за архитектурой. - Рефакторинг рискован и не вознаграждается. Перестройка затрагивает код, который модель не просили менять, и может сломать вызывающий код и тесты. Угодливый ассистент в духе «просто заставь работать» минимизирует радиус поражения, не трогая работающий код, — именно это поведение OX измерил как «избегание рефакторинга» (Avoidance of Refactors) в 80–90% сгенерированного ИИ кода: ИИ «превосходно генерирует новый код, но никогда не возвращается, чтобы улучшить архитектуру».
- Генерация дёшева, удаление пугает. Выдать 40 новых строк модели ничего не стоит; убедить её удалить и перестроить 40 существующих строк — значит идти против её инстинкта сохранять.
Чем это вредит
- Рефакторинг исчезает из истории. Анализ GitClear 2025 года, охвативший 211 млн изменённых строк, показал, что доля строк, связанных с рефакторингом («перемещённых»), упала с 25% изменённых строк в 2021 году до менее чем 10% в 2024-м, а доля скопированных строк выросла с 8,3% до 12,3% и в 2024 году впервые превысила долю перемещённых.
- Сложность накапливается. Каждый навешанный флаг удваивает число комбинаций, которые должна обрабатывать функция и покрывать тесты; каждый лишний
else ifделает следующее изменение рискованнее — и следующий промпт тоже добавляет, а не перестраивает. - Накопление техдолга. Рефакторинг — это переработка, удерживающая энтропию на низком уровне; подавляя его, вы создаёте долг, который никогда не погашается. Кодовая база растёт быстрее, чем улучшается.
- Нагрузка на ревью. Ревьюеры получают диффы, целиком состоящие из добавлений к и без того длинным функциям; понять, как новая ветка взаимодействует с дюжиной веток выше, труднее, чем проверить функцию, которую перестроили под изменение.
##Лечение
Тактики промптинга / ревью
- Сначала сделайте изменение лёгким. Совет Кента Бека — «для каждого нужного изменения сначала сделайте изменение лёгким (внимание: это может быть трудно), а затем сделайте лёгкое изменение» — напрямую превращается в промпт: «Прежде чем добавлять правило купона, перестрой
priceOrderтак, чтобы новое правило добавлялось одной строкой; поведение не меняй и прогони тесты. Затем добавь функциональность». - Запретите новые параметры-флаги. «Не добавляй булевы параметры и параметры режима к существующим функциям; если поведение должно различаться, сначала предложи перестройку».
- Ограничивайте сложность, а не только проверяйте корректность. Требуйте, чтобы ассистент запускал ESLint
complexity,max-paramsиmax-lines-per-functionи правило когнитивной сложности Sonar и сообщал значения до и после для каждой затронутой функции; любой рост требует обоснования. - Просите рефакторинг отдельным шагом. Генерация и перестройка — разные задачи: сначала «заставь работать», затем отдельное изменение без смены поведения — «теперь перестрой эту функцию так, чтобы ветки стали данными или отдельными функциями», — с зелёными тестами до и после.
- Следите за горячими точками. Отслеживайте произведение частоты изменений на сложность (Code Maat или просто история git) для файлов, которые растут в каждом коммите, написанном ИИ, и планируйте их перестройку до того, как туда придёт следующая функциональность.
Собственно рефакторинг — Разбиение условного оператора и Извлечение метода дают имя каждой ветке; Замена параметра набором специализированных методов или Замена параметров объектом избавляют от параметров-флагов; а Замена условного оператора полиморфизмом (или таблица поиска) берёт на себя ветки, выбирающие поведение по типу или коду.
// После: каждое правило — маленькая функция; priceOrder только их компонует.
type PriceRule = (total: number) => number;
const subtotalOf = (items: Item[]) =>
items.reduce((s, i) => s + i.price * i.qty, 0);
const VAT_RATE = 1.2;
const discount = (rate: number): PriceRule => (t) => t * (1 - rate);
const wholesale: PriceRule = (t) => (t > 1000 ? t * 0.9 : t > 500 ? t * 0.95 : t);
const COUPONS: Record<string, PriceRule> = {
SPRING: (t) => t - 10,
VIP: (t) => t * 0.85,
};
const vat: PriceRule = (t) => t * VAT_RATE;
function priceOrder(items: Item[], rules: PriceRule[]): number {
return rules.reduce((total, rule) => rule(total), subtotalOf(items));
}
// Вызывающий код сам перечисляет нужные правила по порядку:
// priceOrder(items, [discount(0.1), wholesale, COUPONS.SPRING, vat]);
Новое правило ценообразования теперь — ещё один элемент, а не ещё один параметр и ветка, и каждое правило можно протестировать отдельно.
Не путайте с запахом Дублирование вместо извлечения: там модель копирует существующую логику в новый почти идентичный блок, и это ловят детекторы клонов. Здесь ничего не копируется — модель продолжает наращивать одну и ту же функцию ветками, флагами и слоями, и это видно по метрикам сложности и размера.
##Обнаружение анализаторами
- SonarQube / SonarSource S3776 — Когнитивная сложность функций не должна быть слишком высокой — отмечает функции, которые продолжают вбирать новые ветки и флаги
- ESLint complexity — Порог цикломатической сложности функции (опция max)
- ESLint max-lines-per-function — Ограничение длины функции — ловит функции, которые только растут
- ESLint max-params — Ограничение числа параметров — ловит флаги и опции, навешанные на существующую сигнатуру
- ESLint max-depth — Ограничение глубины вложенности блоков — ловит лестницы if/else, надстроенные внутри старых веток
- Code Maat entity-churn — Частота изменений файла по истории git — вместе со сложностью показывает горячие точки, растущие от коммита к коммиту