Питання 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 (ліміт 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]]