Вопрос 22 · Раздел 18

Какие антипаттерны противоречат SOLID принципам?

Представьте, что вы строите дом: антипаттерн — это когда все провода от розеток ведут в одну коробку. Сегодня работает, но если нужно добавить новую розетку — придётся переделыв...

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Антипаттерны — это типичные ошибки в проектировании кода, которые кажутся удобными сейчас, но создают большие проблемы в будущем. Большинство из них прямо противоречат принципам SOLID.

Представьте, что вы строите дом: антипаттерн — это когда все провода от розеток ведут в одну коробку. Сегодня работает, но если нужно добавить новую розетку — придётся переделывать всё.

Основные антипаттерны

1. God Object (Божественный объект) Один класс делает всё: работает с БД, отправляет email, генерирует отчёты, обрабатывает платежи.

  • Нарушает: SRP — у класса слишком много ответственностей
  • Признак: Класс с 1000+ строк и 30+ зависимостями
// God Object — делает ВСЁ
public class ApplicationManager {
    public void createUser(User user) { /* ... */ }
    public void sendEmail(User user) { /* ... */ }
    public void generateReport() { /* ... */ }
    public void processPayment(BigDecimal amount) { /* ... */ }
    public void connectToDatabase() { /* ... */ }
    // ещё 50 методов...
}

2. Anemic Domain Model (Анемичная модель) Классы-сущности содержат только поля и геттеры/сеттеры, а вся логика вынесена в гигантские сервисы.

  • Нарушает: Инкапсуляцию — данные и логика разделены
// Анемичная модель — только данные, без логики
public class Order {
    private Long id;
    private BigDecimal total;
    private List<OrderItem> items;
    // только геттеры и сеттеры
}

// Вся логика здесь
public class OrderService {
    public BigDecimal calculateTotal(Order order) { /* ... */ }
    public void addItem(Order order, OrderItem item) { /* ... */ }
    public boolean isValid(Order order) { /* ... */ }
}

3. Feature Envy (Зависть к функциям) Метод класса A постоянно обращается к данным и методам класса B. Он “завидует” классу B и должен принадлежать ему.

  • Нарушает: Инкапсуляцию и SRP
// Feature Envy: метод зависит от данных другого класса
public class OrderReport {
    public String formatOrder(Order order) {
        return "Order #" + order.getId()
            + " Total: " + order.getTotal()
            + " Items: " + order.getItems().size()
            + " Customer: " + order.getCustomer().getName();
    }
}
// Этот метод "завидует" Order и должен быть в нём

4. Circular Dependency (Циклическая зависимость) Сервис A зависит от B, а B зависит от A.

  • Нарушает: DIP — невозможно использовать модули по отдельности
// Circular Dependency: A → B → A
@Service
public class OrderService {
    private final PaymentService paymentService; // зависит от PaymentService
}

@Service
public class PaymentService {
    private final OrderService orderService; // зависит от OrderService
}

Когда это встречается

  • В быстро растущих стартапах без code review
  • Когда разработчик не знает SOLID
  • Когда “потом отрефакторим” никогда не наступает

🟡 Middle Level

STUPID — антагоним SOLID (мнемоника, описывающая антипаттерны, противоположные SOLID)

STUPID Описание Нарушает SOLID
Singleton Глобальное состояние, мешающее тестированию DIP
Tight Coupling Сильная связность модулей DIP, SRP
Untestability Невозможность изолированного тестирования DIP
Premature Optimization Оптимизация до выявления реальных узких мест OCP
Indistinct Names Нечёткие имена классов и методов SRP
Duplication Дублирование кода DRY (родственник SOLID)

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

Система без чёткой архитектуры, где зависимости переплетены хаотично.

  • Нарушает: Все принципы SOLID
  • Признак: Граф зависимостей похож на запутанный клубок ниток
  • Лечение: Паттерн Strangler Fig — постепенная замена модулей

Stovepipe System (Система-дымоход)

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

  • Нарушает: DIP и ISP
  • Следствие: Невозможность интеграции и огромные затраты на поддержку

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

  1. Ошибка: “Это работает, значит архитектура нормальная.” Решение: Работает сегодня — не значит будет работать завтра. Оценивайте стоимость добавления новой фичи.

  2. Ошибка: “God Object — это просто ‘удобный’ центральный класс.” Решение: Если класс знает про БД, email, PDF и платежи — он станет точкой отказа и bottleneck.

  3. Ошибка: “Circular Dependency — Spring разрулит через прокси.” Решение: Spring действительно создаст прокси, но это маскировка проблемы. Используйте event-driven подход или выделите общий интерфейс.

Когда НЕ стоит бороться с антипаттернами

  • Legacy на поддержке: Если система скоро будет заменена, рефакторинг не окупится
  • Прототип: Для MVP скорость важнее архитектуры
  • Одноразовый скрипт: Утилита для разового импорта данных

Сравнение антипаттернов

Антипаттерн Сложность обнаружения Сложность исправления Влияние на команду
God Object Легко (по размеру класса) Средняя (рефакторинг) Блокирует параллельную работу
Anemic Model Средне (нужен domain analysis) Высокая (перенос логики) Сервисы растут бесконтрольно
Circular Dependency Легко (startup error) Средняя (event/интерфейс) Spring proxy overhead
Big Ball of Mud Легко (общее ощущение) Очень высокая Парализует разработку
Feature Envy Средне (нужен static analysis) Низкая (перенести метод) Запутанный код

🔴 Senior Level

Internal Implementation: Почему антипаттерны “прилипают”

God Object и когнитивная нагрузка: Человеческий мозг может удерживать 7±2 элементов в рабочей памяти. God Object с 50 методами и 30 полями превышает этот лимит в 5-10 раз. Разработчики избегают модифицировать то, что не могут понять — код “застывает”.

Circular Dependency и JVM classloader:

OrderService.class → загрузка → нужен PaymentService
PaymentService.class → загрузка → нужен OrderService

Spring обходит это через CGLIB прокси: создаёт подкласс-обёртку. Но это означает:

  • Дополнительная индирекция на каждый вызов (~1-3нс overhead)
  • this внутри метода ссылается на прокси, не на реальный объект
  • @Transactional на таких методах может работать некорректно (proxy-self-invocation problem)

Anemic Model и ORM: Hibernate/JPA требует no-arg constructor, геттеры/сеттеры, equals/hashCode по ID. Это принуждает к анемичной модели. Но это не “нарушение” — это архитектурный trade-off между OOP purity и ORM compatibility.

Архитектурные Trade-offs

Anemic Domain Model:

  • ✅ Плюсы: Совместимость с ORM (Hibernate); простая сериализация (JSON); лёгкое понимание junior-разработчиками
  • ❌ Минусы: Бизнес-логика размазана по сервисам; сервисы растут до God Objects; нет инкапсуляции invariants

Rich Domain Model:

  • ✅ Плюсы: Логика рядом с данными; инварианты защищены; тестируемость; DDD-совместимость
  • ❌ Минусы: Сложность интеграции с ORM; требует зрелой команды; сложнее сериализовать

Принятие антипаттернов (Pragmatic):

  • ✅ Плюсы: Скорость разработки; меньше boilerplate; проще онбординг
  • ❌ Минусы: Технический долг растёт; через 2 года — Big Ball of Mud; стоимость изменений экспоненциальна

Edge Cases

  1. Static Utility Classes: Math, Collections, StringUtils — формально нарушают DIP (нельзя заменить реализацию). Но они стабильны и не зависят от домена. Решение: Принимайте как допустимое исключение. Они не меняются и не несут состояния.

  2. Framework Annotations: @Entity, @Service, @RestController — привязка к фреймворку нарушает DIP. Но без них фреймворк не работает. Решение: Изолируйте аннотации в Infrastructure Layer. Domain Layer не должен зависеть от Spring/Hibernate.

  3. DTO Classes: Классы для передачи данных между слоями. Формально — анемичная модель. Но их единственная ответственность — транспорт. Решение: Используйте records (Java 14+) для иммутабельных DTO. Это не антипаттерн, а паттерн.

  4. Partial God Object: Класс с 5000 строк, где 4000 — сгенерированный код (мапперы, билдеры). Решение: Исключите generated code из метрик. SonarQube поддерживает @Generated annotation exclusion.

Производительность

God Object и contention:

Запрос 1 → GodObject.methodA() → lock на fieldX
Запрос 2 → GodObject.methodB() → lock на fieldX (конкуренция!)

God-объекты часто становятся точками синхронизации. Если 10 потоков вызывают разные методы одного объекта с shared mutable state — вы получите contention (конкуренцию за блокировки). При высокой конкуренции throughput заметно деградирует.

Speculative Generality и память: Антипаттерн “абстракции про запас” (создание интерфейсов и слоёв для гипотетических будущих требований) раздувает иерархию классов:

  • Каждый интерфейс = class-объект в metaspace (~1-3KB)
  • 100 неиспользуемых интерфейсов = ~100-300KB metaspace
  • Каждая фабрика = дополнительный объект для DI container
  • На практике: проекты с speculative generality имеют значительно больше классов, большинство из которых имеют лишь одну реализацию

Circular Dependency и startup time: Spring при обнаружении circular dependencies:

  1. Пробует constructor injection → fail
  2. Пробует setter injection + CGLIB proxy → success (с warning)
  3. Proxy creation добавляет ~5-10мс на каждую зависимость
  4. При 50 circular dependencies — +250-500мс к startup time

Production Experience

War Story: Payment Processing System (2022)

Финтех-стартап, обработка 50K платежей/день. PaymentManager — God Object на 15,000 строк, 89 зависимостей. Работал 2 года, потом:

  • Добавление нового провайдера оплаты: 3-4 недели
  • Регрессионные баги: 5-7 на релиз
  • Merge-конфликты: ежедневно
  • Новый разработчик: 3 месяца до первого PR

Рефакторинг через Strangler Fig:

  1. Выделили интерфейс PaymentProvider из методов PaymentManager
  2. Постепенно мигрировали каждый провайдер в отдельный класс
  3. Оркестратор стал координатором (300 строк вместо 15,000)
  4. Результат: новый провайдер за 2 дня, 0 регрессионных багов за квартал

War Story: Legacy Banking System (2024)

Банк, ядро на Java 8 (мигрировали на 17). TransactionProcessor — Big Ball of Mud, 40,000 строк. Граф зависимостей: 200+ классов, каждый зависит от каждого.

Диагностика:

SonarQube metrics:
- Technical Debt: оценка в годах работы по исправлению (условная метрика)
- Cognitive Complexity: 450 (limit 15)
- LCOM4: 12.0
- Circular Dependencies: 23

Подход:

  1. Заморозили TransactionProcessor — больше никаких изменений
  2. Построили Strangler Fig вокруг него: новый TransactionEngine с чистым SOLID
  3. API Gateway маршрутизировал: новые типы транзакций → новый engine, старые → legacy
  4. За 8 месяцев мигрировали 100% трафика
  5. Результат: Technical Debt значительно снизился

Monitoring и диагностика

  • Static Analysis Tools:
    • SonarQube: Cognitive Complexity, LCOM4, Number of Dependencies, Duplications
    • PMD: God Class detection, Excessive Imports, Coupling Between Objects
    • Checkstyle: Class Data Abstraction Coupling (CDAC)
  • ArchUnit — автоматические проверки в CI/CD: ```java @ArchTest static final ArchRule no_god_objects = noClasses() .that().resideInAPackage(“..service..”) .should().haveMoreThan(10Dependencies()) .orShould().haveMoreThan(50Methods()) .as(“No God objects in service layer”);

@ArchTest static final ArchRule no_circular_dependencies = slices() .matching(“com.company.(*)..”) .should().beFreeOfCycles() .as(“No circular dependencies between modules”);

@ArchTest static final ArchRule no_feature_envy = noClasses() .that().haveSimpleNameEndingWith(“Service”) .should().accessClassesThat().haveSimpleNameEndingWith(“Entity”) .moreThan(3) .as(“Services should not envy entities too much”); ```

  • Dependency Graph Visualization:
    • JDepend: Метрики связности и абстрактности
    • IntelliJ IDEA Dependency Matrix: Визуальный граф
    • Structure101: Автоматическое обнаружение cycles и layer violations
  • Ключевые метрики: | Метрика | Норма | Критично | | ———————- | ——- | ———- | | Dependencies per class | <= 10 | > 20 | | LCOM4 | <= 1 | > 3 | | Cognitive Complexity | <= 15 | > 50 | | Circular dependencies | 0 | > 3 | | Class size (lines) | <= 300 | > 1000 |

Best Practices для Highload

  • Immutability by Default: God Objects почти всегда имеют mutable shared state. Делайте объекты иммутабельными — это автоматически предотвращает contention.
  • Event-Driven Decoupling: Вместо direct dependencies между сервисами используйте event bus (Kafka, Spring Events). Это разрывает circular dependencies.
  • Bounded Contexts (DDD): Разделяйте систему по бизнес-контекстам. God Object нарушает boundaries — не допускайте cross-context calls напрямую.
  • Feature Flags для Strangler Fig: При рефакторинге God Object держите старый и новый код одновременно. Переключайте через feature flag, не через деплой.
  • AI-assisted Refactoring: Современные инструменты (IntelliJ IDEA AI Assistant, CodeRabbit) автоматически предлагают разбиение God Objects на основе анализа ко-локализации методов.
  • Module System Enforcement (JPMS): Java 9+ module-info.java формализует зависимости. Circular dependencies становятся compile-time error, а не runtime problem.
  • ArchUnit as Code: Тесты на архитектуру становятся first-class citizens в CI/CD. Anti-pattern detection — это не code review opinion, а failing test.

Резюме

  • Антипаттерны — это симптомы “гнилого дизайна”. Большинство из них — прямое нарушение SOLID.
  • Anemic Domain Model — самый частый и спорный антипаттерн в Java Enterprise.
  • Боритесь со STUPID кодом через культуру Code Review и автоматический static analysis.
  • SOLID — не религия, а лекарство от антипаттернов. Применяйте прагматично.
  • Лучший способ борьбы с Big Ball of Mud — не допускать его появления.

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

Обязательно знать:

  • STUPID — антагоним SOLID: Singleton, Tight Coupling, Untestability, Premature Optimization, Indistinct Names, Duplication
  • God Object — один класс делает всё, нарушает SRP, >1000 строк, >30 зависимостей
  • Anemic Domain Model — только данные, без логики; вся логика в гигантских сервисах
  • Circular Dependency — Spring обходит через CGLIB прокси, но это маскировка проблемы дизайна
  • Big Ball of Mud — система без архитектуры, лечится Strangler Fig (постепенная замена)
  • Feature Envy — метод класса A постоянно обращается к данным класса B; должен быть в B
  • Speculative Generality — абстракции “про запас”, раздувает иерархию без пользы

Частые уточняющие вопросы:

  • Почему Circular Dependency — проблема? — CGLIB прокси: +1-3ns overhead, this = прокси не реальный объект, @Transactional может работать некорректно
  • Anemic Model — это всегда плохо? — Нет, для ORM (Hibernate) — trade-off; изолируйте в Data Access Layer
  • Как рефакторить Big Ball of Mud? — Заморозить старый код → Strangler Fig вокруг → API Gateway маршрутизирует → постепенная миграция
  • Метрики антипаттернов? — Dependencies > 20, LCOM4 > 3, Cognitive Complexity > 50, Class size > 1000 строк

Красные флаги (НЕ говорить):

  • “Это работает, значит архитектура нормальная” (работает сегодня — не значит поддерживаемо завтра)
  • “Circular Dependency — Spring разрулит через прокси” (маскировка проблемы, proxy overhead)
  • “God Object — удобный центральный класс” (точка отказа, bottleneck, невозможно тестировать)

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

  • [[1. Что такое принцип Single Responsibility и как его применять]]
  • [[18. Как рефакторить God Object (божественный объект)]]
  • [[9. Зачем вообще нужны принципы SOLID]]
  • [[8. Что такое принцип Dependency Inversion]]