Які антипатерни суперечать принципам SOLID?
Уявіть, що ви будуєте будинок: антипатерн — це коли всі дроти від розеток ведуть в одну коробку. Сьогодні працює, але якщо потрібно додати нову розетку — доведеться переробляти...
🟢 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
- Наслідок: Неможливість інтеграції і величезні витрати на підтримку
Типові помилки
-
Помилка: “Це працює, значить архітектура нормальна.” Рішення: Працює сьогодні — не значить працюватиме завтра. Оцінюйте вартість додавання нової фічі.
-
Помилка: “God Object — це просто ‘зручний’ центральний клас.” Рішення: Якщо клас знає про БД, email, PDF і платежі — він стане точкою відмови і bottleneck.
-
Помилка: “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
-
Static Utility Classes:
Math,Collections,StringUtils— формально порушують DIP (не можна замінити реалізацію). Але вони стабільні і не залежать від домену. Рішення: Приймайте як допустимий виняток. Вони не змінюються і не несуть стану. -
Framework Annotations:
@Entity,@Service,@RestController— прив’язка до фреймворку порушує DIP. Але без них фреймворк не працює. Рішення: Ізолюйте анотації в Infrastructure Layer. Domain Layer не повинен залежати від Spring/Hibernate. -
DTO Classes: Класи для передачі даних між шарами. Формально — анемічна модель. Але їх єдина відповідальність — транспорт. Рішення: Використовуйте records (Java 14+) для іммутабельних DTO. Це не антипатерн, а патерн.
-
Partial God Object: Клас з 5000 рядків, де 4000 — згенерований код (маппери, білдери). Рішення: Виключіть generated code з метрик. SonarQube підтримує
@Generatedannotation 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:
- Пробує constructor injection → fail
- Пробує setter injection + CGLIB proxy → success (з warning)
- Proxy creation додає ~5-10мс на кожну залежність
- При 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:
- Виділили інтерфейс
PaymentProviderз методівPaymentManager - Поступово мігрували кожен провайдер в окремий клас
- Оркестратор став координатором (300 рядків замість 15,000)
- Результат: новий провайдер за 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
Підхід:
- Заморозили
TransactionProcessor— більше жодних змін - Побудували Strangler Fig навколо нього: новий
TransactionEngineз чистим SOLID - API Gateway маршрутизував: нові типи транзакцій → новий engine, старі → legacy
- За 8 місяців мігрували 100% трафіку
- Результат: 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, не через деплой.
Future Trends
- 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]]