Логотип ConstructiCat
CodeBust.
Навигация по разделу ▾

Избегание рефакторинга.

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

##Признаки и симптомы

Ревьюер видит дифф, почти целиком состоящий из добавлений к уже существующему коду. Модель решила задачу, надстроив ещё одну ветку, флаг или слой над функцией, на которую ей указали, вместо того чтобы перестроить эту функцию так, чтобы изменение в неё вписалось. Характерные признаки:

  • Диффы из одних добавлений: ни одной перемещённой, переименованной или удалённой строки; затронутая функция только удлиняется.
  • Параметры-флаги: 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 — вместе со сложностью показывает горячие точки, растущие от коммита к коммиту