---
title: "Поверхностная абстракция"
type: "ai-smell"
slug: "shallow-abstraction"
url: "http://localhost:3000/ru/ai-smells/shallow-abstraction.md"
category: "Структура"
description: "ИИ-ассистент оборачивает код в лишние функции, классы или интерфейсы, добавляющие слой косвенности, не скрывая никакой сложности и не давая переиспользования, — абстракции, которые лишь пробрасывают вызов к единственной точке вызова."
---
# Поверхностная абстракция

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

## Signs and Symptoms

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

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

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

Это классический запах **Middle Man** / **Lazy Class** / **Speculative Generality** и обратная сторона находки OX Security об «избыточной специализации» (гиперспецифичные одноразовые решения вместо обобщаемых компонентов).

```ts
// ИИ добавил три «слоя», каждый из которых пробрасывает к следующему.
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:

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

```

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

```ts
const name = userRepository.getName(id);

```

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

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

```

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

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

```

## Detected by

- **ESLint** `no-useless-constructor` — Транзитный конструктор, который только делегирует super или пуст (https://eslint.org/docs/latest/rules/no-useless-constructor)
- **ESLint** `no-useless-rename` — Избыточное переименование import/export/деструктуризации, пробрасывающее имя без изменений (https://eslint.org/docs/latest/rules/no-useless-rename)
- **PMD (Java)** `UselessOverridingMethod` — Переопределяющий метод, который лишь вызывает super с теми же аргументами (https://pmd.github.io/pmd/pmd_rules_java_design.html)
- **SonarSource (Sonar S4144)** `Functions/methods should not have identical implementations` — Помечает дублированные одноразовые абстракции с идентичными телами (https://rules.sonarsource.com/java/RSPEC-4144/)
- **jscpd** `duplication threshold` — Обнаруживает почти-дублирующие одноразовые хелперы, которые поверхностные абстракции не сводят воедино (https://github.com/kucherenko/jscpd)
