ConstructiCat Logo
CodeBust.
Browse section ▾

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

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

##Signs and Symptoms

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

  • Копипаст с правками: новая функция на 90% идентична существующей, отличаясь литералом, именем поля или одним лишним if.
  • Параллельные почти-дубликаты: formatUserCsv, formatAdminCsv, formatGuestCsv — все вручную пишут один и тот же цикл вместо одной параметризованной функции.
  • Разрастание ветвлений вместо выделения: длинный метод обрастает ещё одним else if вместо того, чтобы модель вынесла стратегию/таблицу поиска.
  • Изобретение заново: написанные вручную deepClone/debounce/парсер дат, тогда как репозиторий уже импортирует lodash, date-fns и т. п. (OX Security называет это «vanilla style».)
  • Избыточная специализация: гиперспецифичный одноразовый хелпер там, где универсальный уже существует двумя файлами дальше.
  • «Дежавю багов»: одну и ту же правку приходится применять в трёх копиях, потому что дубликат так и не свели по DRY.
// Уже есть в репозитории:
function priceWithTax(items: Item[]) {
  const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);
  return subtotal * 1.2; // НДС 20%
}

// Что модель добавляет для нового случая со "скидкой" —
// целая вторая копия вместо параметра:
function priceWithTaxDiscounted(items: Item[], discount: number) {
  const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0); // дублируется
  return subtotal * (1 - discount) * 1.2;                          // дублированная логика НДС
}

Запах структурный, поэтому он заметнее всего по всему изменению целиком, а не по одному фрагменту: много зелёного, мало перемещённого кода и сканер дублирования, загорающийся на новых строках.

##Reasons for the Problem

Почему модели это порождают

  • Аддитивное смещение предсказания следующего токена. LLM дополняет тот промпт, который находится перед ней. Выдать самодостаточный новый блок — это локально наиболее вероятное и наименее рискованное продолжение; редактирование удалённой абстракции требует удерживать в рабочем контексте весь модуль и предсказать глобально согласованную правку, что сложнее и не вознаграждается непосредственным промптом.
  • Скудный контекст репозитория. Кодовые ассистенты редко загружают всю кодовую базу. Если модель не видит существующий хелпер, библиотеку или базовый класс, она не может его переиспользовать — поэтому она пишет заново. Отчёт OX Security описывает это как эффект «армии джуниоров»: масса локально работающего кода без архитектурной памяти.
  • Рефакторинг рискован и не вознаграждается. Перестройка затрагивает код, который модель не просили менять, и может сломать вызывающий код и тесты. Угодливый ассистент в духе «просто заставь работать» минимизирует радиус поражения, не трогая работающий код, — ровно то поведение, которое OX измерил как «избегание рефакторингов» в 80–90% сгенерированного ИИ кода и «избыточную специализацию» (одноразовое вместо переиспользуемого) ещё в 80–90%.
  • Устаревание обучающих данных. Модель может не знать, что репозиторий перешёл на util-модуль или обновил библиотеку уже после её момента отсечения, поэтому вручную пишет то, что уже существует.
  • Генерация дёшева, удаление пугает. Выдать 40 новых строк модели ничего не стоит; убедить её удалить и консолидировать 40 существующих строк противоречит её инстинкту сохранять.

Чем это вредит

  • Дублирование накапливается. Анализ GitClear 2025 года, охвативший 211 млн изменённых строк, показал, что доля скопированных строк выросла с ~8,3% (2021) до 12,3% (2024) — это первый год, когда дублирование превысило «перемещённый» (отрефакторенный) код, — тогда как доля отрефакторенных строк упала с ~25% до менее чем 10%, то есть примерно на 60%. Число блоков из 5+ дублирующихся строк выросло примерно в 8 раз в 2024 году.
  • Сопровождаемость и корректность. Каждый клон — это место, где про будущую правку можно забыть: «дежавю багов», когда один и тот же дефект повторяется и его приходится латать N раз (OX: 70–80% ИИ-кода нарушают принципы переиспользования именно так).
  • Накопление техдолга. Рефакторинг — это переработка, удерживающая энтропию на низком уровне; подавляя его, вы создаёте долг, который никогда не погашается. Кодовая база растёт быстрее, чем улучшается.
  • Нагрузка на ревью. Ревьюерам теперь приходится на глаз сравнивать почти идентичные блоки, чтобы убедиться, что они намеренно одинаковы, — это сдвиг когнитивной нагрузки, который описывает обсуждение «запахов кода для ИИ-агентов» на Stack Overflow: работа смещается от написания к ревью и консолидации.

##Treatment

Тактики промптинга / ревью

  • Укажите модели на то, что уже есть. «Прежде чем добавлять код, поищи в репозитории существующий хелпер/util/базовый класс и переиспользуй его; если ничего не подходит, обобщи ближайший». Вставьте соответствующий модуль в контекст, чтобы модель действительно видела абстракцию.
  • Ограничьте форму диффа. «Предпочитай редактировать существующие функции, а не добавлять новые. Если два пути исполнения разделяют общую логику, вынеси общую функцию (Extract Function), а не дублируй».
  • Заставьте её запускать инструменты. Требуйте, чтобы ассистент запускал сканер дублирования (jscpd / PMD CPD) и линтер и устранял любые новые находки дублированных блоков, прежде чем объявить задачу выполненной, — это превращает невидимый запах в падающий гейт.
  • Запрашивайте рефакторинг явно, отдельным шагом. Генерация и консолидация — разные задачи; сначала «заставь работать», затем отдельно «теперь приведи к DRY и убери дублирование» — это модели делают гораздо лучше, когда их просят напрямую.
  • Добавьте гейт качества в CI, чтобы дублирование не нарастало незаметно (порог дублированных строк в Sonar или jscpd --threshold).

Собственно рефакторинг — назовите классические приёмы: Extract Function, Parameterize Function и Pull Up / Replace Conditional with Polymorphism или таблица поиска, чтобы устранить Duplicate Code.

// До: две почти идентичные функции (рефакторинг избегается)
function priceWithTax(items: Item[]) {
  const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);
  return subtotal * 1.2;
}
function priceWithTaxDiscounted(items: Item[], discount: number) {
  const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);
  return subtotal * (1 - discount) * 1.2;
}

// После: Extract Function + Parameterize Function
const VAT = 1.2;
const subtotalOf = (items: Item[]) =>
  items.reduce((s, i) => s + i.price * i.qty, 0);

function priceWithTax(items: Item[], discount = 0) {
  return subtotalOf(items) * (1 - discount) * VAT;
}

Единый источник истины для промежуточной суммы и НДС: будущее изменение налогового правила теперь становится правкой в одну строку вместо поиска по N местам.

##Detected by