📐 Розділ 18 · Питання #22

Які антипатерни суперечать принципам SOLID

Кожен із п'яти принципів SOLID був сформульований як пряма протиотрута від конкретної групи архітектурних антипатернів.


🟢 Junior Level

Антипатерни (Anti-patterns) — це типові, регулярно повторювані помилки проєктування архітектури, які на перший погляд здаються простими і швидкими, але в довгостроковій перспективі роблять кодову базу крихкою, нетестованою та дорогою в підтримці.

Кожен із п’яти принципів SOLID був сформульований як пряма протиотрута від конкретної групи архітектурних антипатернів.

5 головних антипатернів проти SOLID

  1. God Object / The Blob (Божественний об’єкт) — порушує SRP:
    • Клас-гігант (2000+ рядків), який робить все: зберігає в БД, перевіряє вхідні дані, шле email і рахує баланс.
  2. Hardcoded Conditionals / Magic Switch — порушує OCP:
    • Монолітні методи з гігантськими ланцюжками if (type == "CARD") ... else if (type == "APPLE_PAY") .... Додавання нової фічі вимагає хірургічного втручання в перевірений код.
  3. Refusal of Bequest (Відмова від спадщини) — порушує LSP:
    • Підклас успадковує базовий клас, але для половини методів викидає throw new UnsupportedOperationException("Not supported") або залишає порожні заглушки.
  4. Fat Interface (Жирний інтерфейс) — порушує ISP:
    • Інтерфейс на 40 методів. Класи змушені реалізовувати десятки чужих методів, які їм ніколи не знадобляться.
  5. 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:

  1. Зрив Constructor Injection: Spring не може побудувати спрямований ациклічний граф (DAG) створення бінів і викидає фатальний BeanCurrentlyInCreationException.
  2. Костиль @Lazy та деградація CGLIB: Якщо розробник ставить @Lazy, Spring впроваджує динамічний CGLIB-проксі.
    • Кожен виклик через такий проксі додає накладні витрати динамічного інтерцептора (~30–50 нс).
    • Виникає Proxy Self-Invocation Problem: виклик методу @Transactional всередині того самого класу йде повз проксі, і транзакція мовчки не відкривається!
  3. Рішення за 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

Коли антипатерни не присікаються на ранніх етапах:

  1. Big Ball of Mud (Великий клубок бруду): Система, в якій будь-яка частина коду може неявно залежати від будь-якої іншої частини. Рефакторинг «в лоб» неможливий.
    • Лікування: Тільки ізоляція фасадами та поступова викатка через Strangler Fig Pattern.
  2. 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) шкідливий?

Відповідь:

  1. Хибне відчуття слабкої зв’язності: інтерфейс просто сліпо повторює всі методи одного класу. При зміні логіки сервісу інтерфейс змінюється синхронно з реалізацією (абстракції немає).
  2. Засмічення навігації в IDE: під час переходу за викликом методу розробник потрапляє в інтерфейс замість реального коду.
  3. Порушення 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 вирішує їх через @Lazy CGLIB-проксі зі штрафом до затримки та ризиком втрати транзакційності.

Чого уникати (Red Flags):

  • ❌ Захищати GoF Singleton із глобальним статичним методом getInstance() (ознака легасі-мислення).
  • ❌ Вважати, що створення пари Interface / Impl для кожного класу рятує від антипатернів (це антипатерн Header Interface).
  • ❌ Ігнорувати проблему Feature Envy (випитування чужих полів замість інкапсульованого виклику методу).

Пов’язані теми