🧩 Раздел 2 · Вопрос #16

Какие антипаттерны вы знаете

Один класс берет на себя слишком много обязанностей (сотни полей, тысячи строк кода), превращая остальные классы в пассивные структуры данных.


🟢 Junior Level

Антипаттерн (Anti-pattern) — это типичный, часто повторяющийся подход к решению задачи, который кажется интуитивно простым или правильным, но на практике приводит к негативным последствиям: усложнению поддержки, скрытым багам, деградации производительности и росту технического долга. В отличие от обычной ошибки, антипаттерн имеет устоявшуюся структуру и документированное правильное решение (рефакторинг).


Бытовая аналогия

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

Базовые антипаттерны в коде

1. God Object (Божественный объект / The Blob)

Один класс берет на себя слишком много обязанностей (сотни полей, тысячи строк кода), превращая остальные классы в пассивные структуры данных.

// ❌ АНТИПАТТЕРН: Класс делает вообще всё
public class OrderManager {
    public void validateCart() { /* ... */ }
    public void calculateDiscounts() { /* ... */ }
    public void processPayment() { /* ... */ }
    public void generatePdfInvoice() { /* ... */ }
    public void sendEmailConfirmation() { /* ... */ }
    public void backupToDatabase() { /* ... */ }
}

// ✅ РЕШЕНИЕ: Разделение по принципу Single Responsibility (SRP)
// PaymentService, InvoiceGenerator, NotificationService, OrderRepository

2. Spaghetti Code (Спагетти-код)

Код с запутанным потоком управления, глубокой вложенностью условий if-else, циклами и отсутствием модульной структуры. Чтение и модификация такого кода превращаются в мучение.

3. Copy-Paste Programming (Программирование копированием)

Дублирование кусков логики по всей кодовой базе вместо создания переиспользуемых методов, функций или утилитных классов.

  • Опасность: Баг, исправленный в одном месте, остается в 10 других скопированных участках (Shotgun Surgery).

4. Magic Numbers & Strings (Магические числа и строки)

Использование безымянных числовых литералов и текстовых констант напрямую в условиях.

// ❌ АНТИПАТТЕРН: Что значит 86400 и "ACTIVE"?
if (user.getStatus().equals("ACTIVE") && duration > 86400) { ... }

// ✅ РЕШЕНИЕ: Именованные константы и Enum
private static final int SECONDS_IN_DAY = 86_400;
if (user.getStatus() == UserStatus.ACTIVE && duration > SECONDS_IN_DAY) { ... }

🟡 Middle Level

Антипаттерны объектно-ориентированного проектирования

1. Golden Hammer (Золотой молоток / Закон инструмента)

«Если единственный инструмент в руках — молоток, любая проблема кажется гвоздем».

Принудительное натягивание полюбившейся технологии или паттерна проектирования на все задачи подряд без учета контекста.

  • Примеры: Использование Kafka для передачи трех сообщений в день; микросервисная архитектура для пет-проекта с 10 пользователями; внедрение паттерна Visitor там, где достаточно обычного полиморфизма.

2. Poltergeist (Полтергейст / Цыганская повозка)

Классы с мимолетным жизненным циклом и без собственного состояния, единственная цель которых — вызвать метод другого класса и исчезнуть. Они создают бесполезный слой косвенности, усложняя трассировку вызовов.

// ❌ АНТИПАТТЕРН: Лишний класс-пустышка
public class OrderProcessorProxyHelper {
    public void execute(Order order) {
        new RealOrderProcessor().process(order);
    }
}

3. Sequential Coupling (Последовательное связывание)

Класс спроектирован так, что его методы обязаны вызываться в строго определенной временной последовательности, но это ограничение не выражено в сигнатурах типов и проверяется только в рантайме.

// ❌ АНТИПАТТЕРН: Неочевидный порядок вызова методов
ReportGenerator report = new ReportGenerator();
report.initTemplate();
report.loadData();
report.renderPdf(); // Если забыть вызвать initTemplate() -> NullPointerException!

// ✅ РЕШЕНИЕ: Builder, Factory или передача параметров через конструктор
ReportGenerator report = ReportGenerator.fromTemplate(template).withData(data);
report.renderPdf();

4. Anemic Domain Model (Анемичная доменная модель)

Сущности бизнес-логики (User, Account, Order) представляют собой «мешки с данными» — чистые геттеры и сеттеры без единого бизнес-метода. Вся бизнес-логика вынесена в огромные сервисные классы (UserService, OrderService).

  • Почему это антипаттерн (по Мартину Фаулеру): Нарушается инкапсуляция ООП, логика валидации размывается по десяткам сервисов, невозможно гарантировать целостность инвариантов сущности.
  • Альтернатива: Rich Domain Model (Богатая доменная модель) в парадигме DDD (Domain-Driven Design), где сущность сама контролирует изменения своих полей (order.cancel(), account.withdraw(amount)).

5. Lava Flow (Поток лавы / Мертвый код)

Слои устаревшего, мертвого или недописанного кода, который все боятся трогать или удалять («Оно как-то работает, тронешь — всё упадет»). Возникает из-за отсутствия автоматизированных тестов и частой смены разработчиков в команде.


Сводная таблица архитектурных симптомов

Антипаттерн Симптом / Признак Последствия Способ лечения
God Object Класс > 1000 строк, десятки инжектов Merge-конфликты, невозможность unit-тестирования Декомпозиция по SRP, фасад
Sequential Coupling Вызовы методов требуют неявного порядка Рантайм-ошибки IllegalStateException Builder, Fluent API, Factory Method
Golden Hammer Избыточно сложный стек под простую задачу Рост расходов на инфраструктуру, оверинжиниринг Принципы KISS, YAGNI, оценка trade-offs
Anemic Model Сервисы с логикой, Entity только с геттерами Размазывание инвариантов, спагетти в сервисах Rich Domain Model (DDD)
Lava Flow Закомментированный код, @Deprecated классы Засорение репозитория, страх рефакторинга Покрытие интеграционными тестами, удаление мертвого кода

🔴 Senior Level

Архитектурные антипаттерны в Enterprise и Spring

1. Big Ball of Mud (Большой комок грязи)

Архитектура приложения полностью деградировала: границы между слоями стерты, контроллеры напрямую вызывают репозитории в обход сервисов, присутствуют циклические зависимости между пакетами.

  • Инструменты защиты на уровне CI/CD: ArchUnit — написание модульных тестов на соблюдение архитектурных правил.
    @ArchTest
    public static final ArchRule no_controllers_in_repositories = 
        noClasses().that().resideInAPackage("..repository..")
                   .should().dependOnClassesThat().resideInAPackage("..controller..");
    

2. Cargo Cult Programming (Программирование культа Карго) в экосистеме Spring

Бессмысленное копирование аннотаций, конфигураций или архитектурных подходов «потому что так везде делают»:

  • @Transactional на каждом методе: Навешивание аннотации на чистые SELECT методы или методы, выполняющие долгие сетевые HTTP-вызовы. Это держит соединение из пула HikariCP открытым на протяжении всего внешнего запроса, приводя к исчерпанию соединений пула (Pool Exhaustion).
  • Field Injection (@Autowired private MyService service;): Нарушает инкапсуляцию, делает класс нетестируемым без Spring-контекста или рефлексии, провоцирует появление циклических зависимостей. Единственный стандарт — Constructor Injection.
  • Бесконтрольный @Async без явного TaskExecutor: Spring использует SimpleAsyncTaskExecutor, который не переиспользует пулы потоков, а порождает новый системный поток ОС на каждую операцию, что при всплеске нагрузки мгновенно приводит к OutOfMemoryError: unable to create new native thread.

3. Distributed Monolith (Распределенный монолит)

Система разбита на десятки микросервисов, но они связаны жесткими синхронными цепочками HTTP/gRPC вызовов и общей базой данных.

  • Проблемы: Каскадные падения (один упавший сервис кладет всю цепочку), распределенные транзакции через 2PC/Saga, ад деплоймента (нельзя развернуть сервис А без одновременного релиза сервисов Б и В).
  • Решение: Асинхронное взаимодействие через очереди (Kafka/RabbitMQ), изоляция баз данных (Database-per-service), соблюдение Bounded Contexts.

4. Premature Optimization (Преждевременная оптимизация)

«Преждевременная оптимизация — корень всех зол в программировании» (Дональд Кнут).

Усложнение кода многопоточными блокировками, самописными пулами объектов и битовыми сдвигами до проведения реальных нагрузочных тестов и профилирования (JProfiler, async-profiler).

  • Результат: Код становится недоступным для понимания и поддержки, а современный JIT-компилятор (C2) не может эффективно оптимизировать переусложненный код.

Метрики качества кода и статический анализ

Для автоматического выявления антипаттернов в production-пайплайнах применяются инструменты статического анализа: SonarQube, SpotBugs, Checkstyle, PMD.

Ключевые измеряемые метрики:

  1. Cyclomatic Complexity (Цикломатическая сложность Маккейба): Количество линейно независимых путей в коде. Порог для метода: $\le 10$.
  2. Cognitive Complexity (Когнитивная сложность): Насколько тяжело человеческому мозгу воспринять логику потока управления (вложенные циклы и условия увеличивают метрику экспоненциально).
  3. Instability ($I$) пакета: $I = \frac{C_e}{C_a + C_e}$, где $C_e$ (Efferent Coupling) — исходящие зависимости, а $C_a$ (Afferent Coupling) — входящие зависимости. Метрика показывает устойчивость модуля к внешним изменениям.

🎯 Шпаргалка для интервью

30-секундный ответ (Elevator Pitch)

«Антипаттерн — это контрпродуктивный шаблон проектирования, решение с предсказуемо негативными последствиями. На уровне кода главные антипаттерны — God Object (нарушение SRP), Sequential Coupling (неявный порядок вызовов), Magic Values и Spaghetti Code. На уровне архитектуры — Golden Hammer (натягивание одной технологии на все задачи), Cargo Cult (слепое копирование аннотаций типа @Transactional без понимания механики), Anemic Domain Model (лишение сущностей бизнес-логики) и Distributed Monolith (микросервисы с жесткой синхронной связанностью). Защита строится на принципах SOLID, KISS, YAGNI, автоматизированном контроле через ArchUnit и статанатическом анализе (SonarQube)».


3–4 каверзных вопроса с ответами

  1. Почему паттерн Singleton часто называют антипаттерном?
    • Ответ: Классический синглтон скрывает зависимости класса внутри кода (getInstance()), вместо явного их объявления в конструкторе. Это нарушает принцип инверсии зависимостей (DIP), делает код нетестируемым (невозможно подменить моком в unit-тестах), хранит глобальное разделяемое изменяемое состояние и порождает race conditions в многопоточной среде. В современных приложениях Singleton реализуется не через статические методы класса, а через жизненный цикл бина в DI-контейнере (Spring Singleton Scope).
  2. Чем опасен Cargo Cult вокруг аннотации @Transactional на методе отправки email?
    • Ответ: Если поместить сетевой вызов (отправка письма через SMTP, вызов стороннего платежного шлюза) внутрь @Transactional, транзакция БД будет оставаться открытой всё время сетевого ожидания (таймауты внешних API могут достигать десятков секунд). Это исчерпает пул соединений к базе (HikariCP pool exhaustion), заблокирует строки в таблицах и приведет к отказу всей системы. Сетевые вызовы должны выполняться вне транзакции (через TransactionSynchronization, Outbox pattern или после фиксации транзакции).
  3. Что такое Anemic Domain Model и почему Мартин Фаулер назвал её антипаттерном?
    • Ответ: Это архитектурный стиль, при котором сущности предметной области лишены логики и содержат только данные (поля, геттеры и сеттеры), а вся бизнес-логика перенесена в сервисный слой. Фаулер назвал это антипаттерном, потому что такой подход противоречит фундаментальной идее ООП — объединению данных и операций над ними в одном объекте. Это ведет к размытию правил валидации по разным сервисам и невозможности гарантировать консистентность состояния сущности.
  4. Как библиотека ArchUnit помогает предотвратить превращение архитектуры в Big Ball of Mud?
    • Ответ: ArchUnit позволяет писать архитектурные правила в виде обычных Java unit-тестов, которые запускаются в CI/CD конвейере. Например, можно запретить циклические зависимости между пакетами (slices().matching("..myapp.(*)..").should().beFreeOfCycles()), запретить контроллерам обращаться к БД минуя сервисы, или обязать все сервисы иметь суффикс Service. Если разработчик нарушит архитектурные границы, сборка упадет на этапе тестов.

Типичные ошибки и Red Flags

  • 🚩 Утверждение: «Я ставлю @Transactional на каждый метод сервиса на всякий случай» (грубейший маркер Cargo Cult программирования).
  • 🚩 Оправдание гигантских сервисов на 3000 строк фразой «Зато весь код в одном месте» (непонимание God Object и принципа SRP).
  • 🚩 Использование Field Injection (@Autowired полей) в новом коде вместо внедрения через конструктор.
  • 🚩 Игнорирование метрик статического анализа и отсутствие архитектурных проверок (ArchUnit) в enterprise-проектах.

Связанные темы