Які антипатерни суперечать принципам SOLID
Кожен із п'яти принципів SOLID був сформульований як пряма протиотрута від конкретної групи архітектурних антипатернів.
🟢 Junior Level
Антипатерни (Anti-patterns) — це типові, регулярно повторювані помилки проєктування архітектури, які на перший погляд здаються простими і швидкими, але в довгостроковій перспективі роблять кодову базу крихкою, нетестованою та дорогою в підтримці.
Кожен із п’яти принципів SOLID був сформульований як пряма протиотрута від конкретної групи архітектурних антипатернів.
5 головних антипатернів проти SOLID
- God Object / The Blob (Божественний об’єкт) — порушує SRP:
- Клас-гігант (2000+ рядків), який робить все: зберігає в БД, перевіряє вхідні дані, шле email і рахує баланс.
- Hardcoded Conditionals / Magic Switch — порушує OCP:
- Монолітні методи з гігантськими ланцюжками
if (type == "CARD") ... else if (type == "APPLE_PAY") .... Додавання нової фічі вимагає хірургічного втручання в перевірений код.
- Монолітні методи з гігантськими ланцюжками
- Refusal of Bequest (Відмова від спадщини) — порушує LSP:
- Підклас успадковує базовий клас, але для половини методів викидає
throw new UnsupportedOperationException("Not supported")або залишає порожні заглушки.
- Підклас успадковує базовий клас, але для половини методів викидає
- Fat Interface (Жирний інтерфейс) — порушує ISP:
- Інтерфейс на 40 методів. Класи змушені реалізовувати десятки чужих методів, які їм ніколи не знадобляться.
- Hard Dependency / Static Cling — порушує DIP:
- Пряме створення інфраструктурних залежностей через оператор
newвсередині бізнес-методів (new PostgresConnection()), що робить неможливим ізольоване тестування.
- Пряме створення інфраструктурних залежностей через оператор
Мнемоніка STUPID: Повний антагоніст SOLID
В індустрії популярна мнемоніка STUPID, що описує шкідливі звички процедурного стилю в ООП:
- S (Singleton): Зловживання синглтонами з глобальним мутабельним станом.
- T (Tight Coupling): Жорстке зачеплення класів без інтерфейсів.
- U (Untestability): Неможливість написати модульний тест без підняття всієї бази та зовнішніх сервісів.
- P (Premature Optimization): Передчасна оптимізація на шкоду читабельності.
- I (Indistinct Names): Розмиті беззмістовні імена (
Manager,DataProcessor,Helper). - D (Duplication): Дублювання логіки (порушення DRY).
🟡 Middle Level
Детальна карта відповідності: Принцип SOLID проти Антипатерна
| Літера SOLID | Інваріант, що захищається | Антипатерни, що суперечать | Симптоми в коді |
|---|---|---|---|
| S (SRP) | У класу одна причина для зміни (один актор). | God Object (The Blob) Feature Envy Accidental Duplication |
Метод одного класу постійно читає гетери іншого класу; LCOM4 $\ge 2$; клас імпортує одночасно SQL, Mail та UI. |
| O (OCP) | Відкритість для розширення, закритість для правок. | Type Code with Switch Speculative Generality |
Постійні зміни одних і тих самих файлів у Git; або зворотна крайність — 15 інтерфейсів без жодної альтернативної реалізації. |
| L (LSP) | Підклас взаємозамінний із базовим типом. | Refusal of Bequest Square-Rectangle Trap Broken Contract |
UnsupportedOperationException; приховане спотворення інваріантів при сетерах; порушення контракту equals/hashCode. |
| I (ISP) | Клієнт залежить лише від того, що використовує. | Fat Interface Header Interface Interface Pollution |
Інтерфейс механічно дублює методи класу (IUserService); порожні методи-заглушки в реалізаціях. |
| D (DIP) | Високий рівень залежить від абстракцій. | Static Cling Service Locator Circular Dependency |
new Service() всередині класу; ServiceLocator.getBean(); взаємні посилання сервісів A -> B -> A. |
Аналіз антипатерна Feature Envy (Заздрість до чужих функцій)
Один із найпідступніших запахів коду за Мартіном Фаулером:
// Антипатерн Feature Envy: метод класу OrderProcessor "заздрить" класу Wallet
public class OrderProcessor {
public void processPayment(User user, BigDecimal amount) {
// Метод лізе в нутрощі User та Wallet, виконуючи розрахунки над чужими даними!
if (user.getWallet().getBalance().compareTo(amount) >= 0) {
BigDecimal newBalance = user.getWallet().getBalance().subtract(amount);
user.getWallet().setBalance(newBalance);
} else {
throw new InsufficientFundsException();
}
}
}
Чому це порушує SRP та інкапсуляцію:
Логіка управління балансом належить гаманцю (Wallet), а не процесору замовлень.
Виправлення (Принцип Tell, Don’t Ask):
user.getWallet().withdraw(amount); // Гаманець сам перевіряє баланс і списує кошти
🔴 Senior Level
Глибокий аналіз: Circular Dependencies та проксі-пастки Spring
Антипатерн Circular Dependency (Циклічна залежність) — прямий наслідок ігнорування DIP та SRP:
@Service
public class OrderService {
private final PaymentService paymentService;
}
@Service
public class PaymentService {
private final OrderService orderService; // ЦИКЛ!
}
Технічні наслідки в JVM:
- Зрив Constructor Injection: Spring не може побудувати спрямований ациклічний граф (DAG) створення бінів і викидає фатальний
BeanCurrentlyInCreationException. - Костиль
@Lazyта деградація CGLIB: Якщо розробник ставить@Lazy, Spring впроваджує динамічний CGLIB-проксі.- Кожен виклик через такий проксі додає накладні витрати динамічного інтерцептора (~30–50 нс).
- Виникає Proxy Self-Invocation Problem: виклик методу
@Transactionalвсередині того самого класу йде повз проксі, і транзакція мовчки не відкривається!
- Рішення за Senior-стандартом:
Розірвати цикл:
- Або виділити третього координатора (Mediator).
- Або перейти на асинхронну шину подій (Domain Events через
ApplicationEventPublisher).
Дилема архітектури: Anemic Domain Model vs Rich Domain Model
Одна з найзапекліших суперечок в enterprise Java:
- Anemic Domain Model (Мартін Фаулер): Сутності — чисті DTO з гетерами та сетерами без логіки. Уся бізнес-логіка зосереджена в класах
Service.- Плюси: Ідеальна сумісність із JPA/Hibernate, простота для новачків.
- Мінуси: Порушення інкапсуляції; сервіси швидко мутують у некеровані God Objects.
- Rich Domain Model (Багата модель у DDD): Сутність сама інкапсулює бізнес-логіку і захищає свої інваріанти (
order.cancel(),order.applyDiscount()).- Плюси: Відповідність SRP та High Cohesion; чиста бізнес-логіка.
- Мінуси: Складність інтеграції з ORM-бібліотеками, які вимагають порожніх конструкторів і мутабельних полів.
Архітектурний крах: Big Ball of Mud та Stovepipe Systems
Коли антипатерни не присікаються на ранніх етапах:
- Big Ball of Mud (Великий клубок бруду):
Система, в якій будь-яка частина коду може неявно залежати від будь-якої іншої частини. Рефакторинг «в лоб» неможливий.
- Лікування: Тільки ізоляція фасадами та поступова викатка через Strangler Fig Pattern.
- Stovepipe Enterprise System (Система димоходів): Кілька незалежних монолітів усередині однієї компанії, які дублюють функції один одного з несумісними форматами даних і протоколами (порушення ISP та DIP на рівні масштабу підприємства).
4 Tricky Questions
1. Чи є патерн Singleton безумовним антипатерном у сучасній Java-розробці?
Відповідь:
Класичний GoF Singleton (public static getInstance() із приватним статичним полем) у сучасному enterprise-коді визнаний антипатерном. Він створює неявну глобальну залежність, робить клас нетестованим у багатопотокових тестах (спільний змінний стан між тестами) та порушує DIP.
Проте концепція синглтона на рівні життєвого циклу фреймворка (Singleton Scope у Spring) — це фундаментальний патерн. У Spring бін створюється в єдиному екземплярі контейнером, впроваджується через конструктор (DIP дотримано) і може бути легко замінений Mock-об’єктом у тестах.
2. Як антипатерн Speculative Generality (Спекулятивна генералізація) пов’язаний із порушенням OCP?
Відповідь: Спекулятивна генералізація — це «вивертання OCP навиворіт». Розробник, начитавшись про OCP, намагається передбачити всі можливі зміни на 5 років вперед і створює 10 шарів інтерфейсів, абстрактних фабрик та адаптерів для функціоналу, у якого завжди буде рівно одна реалізація. Це перетворює код на переускладнений конструктор, підвищує когнітивне навантаження і порушує принцип YAGNI. Справжній OCP вимагає створення абстракцій лише в тих точках, де варіативність реально підтверджена вимогами бізнесу.
3. У чому різниця між запахом коду Feature Envy та порушенням закону Деметри (Law of Demeter)?
Відповідь:
- Feature Envy (Заздрість до функціоналу): метод звертається до безлічі полів або гетерів іншого класу частіше, ніж до своїх власних даних. Проблема в тому, де живе метод: він обчислює чужу логіку ззовні.
- Law of Demeter (Train Wreck): метод викликає ланцюжок викликів углиб через кілька посередників (
a.getB().getC().getD()). Проблема в навігації закритою структурою: метод знає структуру зв’язків далеко за межами свого прямого сусіда. Обидва антипатерни лікуються перенесенням методу ближче до даних (Tell, Don’t Ask).
4. Чому антипатерн Header Interface (інтерфейс IUserService з однією реалізацією UserServiceImpl) шкідливий?
Відповідь:
- Хибне відчуття слабкої зв’язності: інтерфейс просто сліпо повторює всі методи одного класу. При зміні логіки сервісу інтерфейс змінюється синхронно з реалізацією (абстракції немає).
- Засмічення навігації в IDE: під час переходу за викликом методу розробник потрапляє в інтерфейс замість реального коду.
- Порушення ISP: такий інтерфейс з часом розростається до 50 методів, і всі клієнти змушені залежати від єдиного широкого контракту. Створювати інтерфейс слід лише при появі другої реалізації або при необхідності ізолювати модуль через Role Interface.
🎯 Шпаргалка для інтерв’ю
Головні тези (30 секунд на відповідь):
- Головні антипатерни:
- Проти SRP: God Object (The Blob), Feature Envy, Anemic Model.
- Проти OCP: Hardcoded Switch / Type Code, Speculative Generality.
- Проти LSP: Refusal of Bequest (
UnsupportedOperationException), мутації сторін у Квадрат/Прямокутник. - Проти ISP: Fat Interface, Header Interface (механічний
IService). - Проти DIP: Hardcoded
new, Service Locator, Circular Dependencies.
- STUPID мнемоніка: Singleton, Tight Coupling, Untestability, Premature Optimization, Indistinct Names, Duplication.
- Небезпека циклічних залежностей: Spring вирішує їх через
@LazyCGLIB-проксі зі штрафом до затримки та ризиком втрати транзакційності.
Чого уникати (Red Flags):
- ❌ Захищати GoF Singleton із глобальним статичним методом
getInstance()(ознака легасі-мислення). - ❌ Вважати, що створення пари
Interface/Implдля кожного класу рятує від антипатернів (це антипатерн Header Interface). - ❌ Ігнорувати проблему Feature Envy (випитування чужих полів замість інкапсульованого виклику методу).