ConstructiCat Logo
CodeBust.
Browse section ▾

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

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

##Signs and Symptoms

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

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

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

Это классический запах Middle Man / Lazy Class / Speculative Generality и обратная сторона находки 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.

##Reasons for the Problem

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

  • Имитация формы хорошей архитектуры. Обучающие корпуса насыщены туториалами и корпоративными лесами — слоями service/repository/factory, DI, интерфейсами с единственной реализацией. Модель сопоставляет церемонию абстракции без её сути, потому что именованный хелпер локально правдоподобен и «выглядит профессионально». Предсказание следующего токена оптимизирует сходство с хорошо структурированным кодом, а не то, оправдывает ли слой своё существование.
  • Нет контекста репозитория. Генерируя локально, модель не видит, что хелпер вызывается один раз или что каноническая абстракция уже существует где-то ещё, — поэтому она изобретает свежие поверхностные слои вместо их переиспользования. Это ровно измеренный GitClear тренд: доля отрефакторенных («перемещённых») строк упала с ~24% (2020) до менее 10% (2024), тогда как доля скопированных строк выросла, а число клонов кода выросло примерно в 4 раза. Модели добавляют структуру, но никогда её не консолидируют.
  • Фиксация «по учебнику» + избыточная специализация. OX Security обнаружила, что ИИ избыточно специализирует в 80–90% сгенерированного кода, выдавая «узкие решения, которые нельзя переиспользовать», и избегает рефакторингов в 80–90% («останавливается на достаточно хорошем»). Поверхностные обёртки — видимый осадок этого.
  • Буквализм промпта и угодливость. Когда сказано «добавь сервисный слой» или «сделай чисто/модульно», модель добавляет слои буквально, даже когда применим YAGNI, — склонность к переусложнению / «god agent», которую описывают практики в обсуждении code-smells-for-AI-agents.
  • Устаревание из-за момента отсечения обучения. Она скорее обернёт устаревший API в тонкий адаптер, чем уйдёт с него.

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

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

##Treatment

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

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

Рефакторинги (назовите их): Inline Function, Inline Class, Remove Middle Man для транзитных слоёв; Collapse Hierarchy / убрать интерфейс для интерфейсов с единственной реализацией. Когда настоящая причина — дублирование, выполните Extract Function один раз в по-настоящему общий, параметризованный хелпер, а не обёртку на каждую точку вызова.

До — Middle Man:

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

После — Inline Function / Remove Middle Man (одна точка вызова, поведение не добавлено):

const name = userRepository.getName(id);

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

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

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

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

##Detected by

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