Поверхностная абстракция.
ИИ-ассистент оборачивает код в лишние функции, классы или интерфейсы, добавляющие слой косвенности, не скрывая никакой сложности и не давая переиспользования, — абстракции, которые лишь пробрасывают вызов к единственной точке вызова.
##Signs and Symptoms
Ревьюер распознаёт поверхностную абстракцию, когда новая функция/класс/интерфейс добавляет имя и слой, но не рычаг: её тело пересказывает её сигнатуру, она пробрасывает прямо к одному нижележащему вызову, она вызывается ровно один раз или она не скрывает ни решения, ни инварианта, ни вариации. Код выглядит слоистым и «корпоративным», но чтобы хоть что-то понять, приходится прочитывать каждый слой — абстракция не инкапсулирует ничего.
Характерные признаки:
- Транзитные функции/методы, чьё тело — единственный делегирующий вызов без добавленного поведения.
- Классы-обёртки (
FooService→FooRepository→db), где каждый слой просто вызывает следующий. - Интерфейсы с единственной реализацией (
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 — Обнаруживает почти-дублирующие одноразовые хелперы, которые поверхностные абстракции не сводят воедино