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

Поверхностная абстракция.

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

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

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

Характерные признаки:

  • Транзитные функции/методы, чьё тело — единственный делегирующий вызов без добавленного поведения.
  • Классы-обёртки (FooService → FooRepository → db), где каждый слой просто вызывает следующий.
  • Интерфейсы с единственной реализацией (IClock только с SystemClock), введённые спекулятивно «ради тестируемости» без второй реализации или фейка.
  • Функции utils/helpers, используемые ровно в одном месте.
  • Избыточные псевдонимы/переименования, пробрасывающие значение без изменений.

Это классические запахи Посредник / Ленивый класс / Теоретическая общность и обратная сторона находки OX Security об избыточной специализации («гиперспецифичные одноразовые решения, которые нельзя приспособить для других целей»).

// ИИ добавил три «слоя», каждый из которых пробрасывает к следующему.
function getUserName(id: string) {
  return fetchUserName(id);            // ничего не добавляет
}
function fetchUserName(id: string) {
  return userRepository.getName(id);   // ничего не добавляет
}
class UserRepository {
  getName(id: string) {
    return db.query("SELECT name FROM users WHERE id = ?", [id]);
  }
}
// getUserName — единственная точка вызова, используется один раз.
// Два из трёх слоёв не добавляют поведения — чистый Middle Man.

##Причины проблемы

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

  • Имитация формы хорошей архитектуры. Обучающие корпуса насыщены туториалами и корпоративными лесами — слоями service/repository/factory, DI, интерфейсами с единственной реализацией. Модель сопоставляет церемонию абстракции без её сути, потому что именованный хелпер локально правдоподобен и «выглядит профессионально». Предсказание следующего токена оптимизирует сходство с хорошо структурированным кодом, а не то, оправдывает ли слой своё существование.
  • Нет контекста репозитория. Генерируя локально, модель не видит, что хелпер вызывается один раз или что каноническая абстракция уже существует в другом месте, — поэтому изобретает новые поверхностные слои вместо переиспользования. Анализ GitClear 2025 года фиксирует ту же тенденцию: доля строк, связанных с рефакторингом («перемещённых»), упала с 25% изменённых строк в 2021 году до менее чем 10% в 2024-м, а доля скопированных строк выросла с 8,3% до 12,3% — в заголовке GitClear это четырёхкратный рост клонов кода. Модели добавляют структуру, но редко её консолидируют.
  • Следование учебнику + избыточная специализация. Отчёт OX Security по более чем 300 репозиториям называет «следование учебнику» (By-The-Book Fixation) — код, который «жёстко придерживается стандартных шаблонов, даже когда более изобретательные решения были бы эффективнее», — и «избыточную специализацию» (Over-Specification) — «гиперспецифичные одноразовые решения, которые нельзя приспособить для других целей», — каждую в 80–90% сгенерированного ИИ кода, а также «избегание рефакторинга» (Avoidance of Refactors, тоже 80–90%): ИИ «превосходно генерирует новый код, но никогда не возвращается, чтобы улучшить архитектуру». Поверхностные обёртки — видимый след этого.
  • Буквализм промпта и угодливость. Получив указание «добавь сервисный слой» или «сделай чисто/модульно», модель буквально добавляет слои, даже когда действует YAGNI, — переусложнение по запросу, а не по необходимости.
  • Устаревание из-за момента отсечения обучения. Она скорее обернёт устаревший API в тонкий адаптер, чем уйдёт с него.

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

  • Сопровождаемость и нагрузка на ревью. Каждое изменение приходится протягивать сквозь мёртвые слои; читателю нужно пройти через каждый промежуточный слой (запах Посредник), чтобы добраться до настоящего вызова. OX сравнивает ИИ с «армией талантливых, но неопытных младших разработчиков», чья выдача опережает ревью, — и код, который выглядит структурированным, но не имеет настоящих швов, как раз проскакивает.
  • Корректность и безопасность. Каждая обёртка и каждый псевдоним — ещё один шаг между входными данными и вызовом, который действительно важен: по нему приходится пройти и ревьюеру, и любому анализу потоков данных или taint-анализу. Транзитная функция, которая «ничего не добавляет», всё равно может проглотить ошибку или спрятать опасный приёмник данных (sink) за безобидным именем.
  • Накопление техдолга. Спекулятивные интерфейсы с единственной реализацией окостеневают; а поскольку каждая поверхностная абстракция одноразова, следующую вариацию копипастят вместо параметризации — напрямую подпитывая рост клонов, измеренный GitClear. Вы платите за косвенность, так и не получив обещанного ею переиспользования.

##Лечение

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

  • Применяйте YAGNI и правило трёх: «Не добавляй функцию/класс/интерфейс ради переиспользования, пока он реально не понадобится в трёх местах вызова; до этого новый слой должен скрывать настоящее решение или инвариант. Встраивай одноразовые хелперы».
  • Принудите переиспользование: «Прежде чем добавлять хелпер/сервис/интерфейс, поищи в репозитории существующий и переиспользуй его». Укажите модели на канонический модуль.
  • Заставьте её обосновать каждый слой: «Перечисли каждую новую функцию/класс и укажи, какую вариацию или инвариант он скрывает; удали все, что лишь пробрасывают».
  • Требуйте, чтобы она запускала линтер (no-useless-constructor, no-useless-rename) и сканирование дублирования (jscpd) и чинила то, что они отметят.

Рефакторинги (назовите их): Встраивание метода, Встраивание класса, Удаление посредника — для транзитных функций; Свёртывание иерархии или удаление интерфейса — для интерфейсов с единственной реализацией. Если настоящая причина — дублирование, примените Извлечение метода один раз и получите действительно общий параметризованный хелпер, а не обёртку на каждую точку вызова.

До — Посредник:

function getUserName(id: string) { return fetchUserName(id); }
function fetchUserName(id: string) { return userRepository.getName(id); }

После — Встраивание метода / Удаление посредника (одна точка вызова, поведение не добавлено):

const name = userRepository.getName(id);

До — спекулятивный интерфейс с единственной реализацией:

interface IClock { now(): number; }
class SystemClock implements IClock { now() { return Date.now(); } }

После — Свёртывание иерархии (используйте конкретный тип; вновь вводите интерфейс только тогда, когда реально появляется вторая реализация, например тестовый фейк):

class SystemClock { now() { return Date.now(); } }

##Обнаружение анализаторами

  • ESLint no-useless-constructor — Транзитный конструктор, который только делегирует super или пуст
  • ESLint no-useless-rename — Избыточное переименование import/export/деструктуризации, пробрасывающее имя без изменений
  • PMD (Java) UselessOverridingMethod — Переопределяющий метод, который лишь вызывает super с теми же аргументами
  • SonarSource (Sonar S4144) S4144 — Помечает дублированные одноразовые абстракции с идентичными телами
  • jscpd --threshold — Обнаруживает почти-дублирующие одноразовые хелперы, которые поверхностные абстракции не сводят воедино