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

Навіщо взагалі потрібні принципи SOLID

Без застосування базових принципів архітектури будь-яка програма з часом перетворюється на так званий Big Ball of Mud («великий клубок бруду»).


🟢 Junior Level

SOLID — це акронім із п’яти фундаментальних принципів об’єктно-орієнтованого проєктування, зібраних Робертом Мартіном (Дядьком Бобом) на початку 2000-х років:

  • S (Single Responsibility Principle): Принцип єдиної відповідальності — у класу має бути лише одна причина для зміни.
  • O (Open-Closed Principle): Принцип відкритості/закритості — сутності відкриті для розширення, але закриті для модифікації.
  • L (Liskov Substitution Principle): Принцип підстановки Лісков — підкласи мають без сюрпризів замінювати свої базові класи.
  • I (Interface Segregation Principle): Принцип розділення інтерфейсів — клієнти не повинні залежати від методів, які вони не використовують.
  • D (Dependency Inversion Principle): Принцип інверсії залежностей — високорівневі та низькорівневі модулі мають залежати від абстракцій.

Навіщо вони потрібні: Боротьба з «гниттям коду»

Без застосування базових принципів архітектури будь-яка програма з часом перетворюється на так званий Big Ball of Mud («великий клубок бруду»).

Роберт Мартін сформулював 4 головні симптоми деградації («гниття») архітектури, які усуває SOLID:

  1. Rigidity (Жорсткість): систему важко змінювати. Спроба змінити один рядок коду тягне за собою каскад правок у десятках інших залежних класів.
  2. Fragility (Крихкість): зміна в одному модулі несподівано ламає логіку в зовсім непов’язаній частині програми, про яку розробник навіть не підозрював.
  3. Immobility (Нерухомість): модуль неможливо перевикористати в іншому сервісі чи проєкті, бо він намертво «приріс» до конкретної бази даних або контексту.
  4. Viscosity (В’язкість): архітектура чинить опір правильним змінам. Розробнику набагато простіше і швидше зробити кривий «костиль», ніж акуратно розширити систему через наявні інтерфейси.

Приклад у коді: Монолітний антипатерн проти SOLID

// ❌ АНТИПАТЕРН: Порушення SRP та DIP (жорстка прив'язка до бази, генерації PDF та відправки пошти)
public class OrderService {
    public void processOrder(Order order) {
        // Логіка збереження в БД
        new MySqlOrderDao().save(order);
        
        // Генерація PDF
        byte[] pdf = new PdfGenerator().generateReceipt(order);
        
        // Відправка email через конкретний SMTP клієнт
        new SmtpEmailSender().send("admin@shop.com", "Order processed", pdf);
    }
}

// ✅ ЗА SOLID: Декомпозиція на єдині ролі та інверсія залежностей (DIP + SRP)
public class OrderProcessor {
    private final OrderRepository repository;
    private final ReceiptGenerator receiptGenerator;
    private final NotificationService notificationService;

    public OrderProcessor(OrderRepository repository,
                          ReceiptGenerator receiptGenerator,
                          NotificationService notificationService) {
        this.repository = repository;
        this.receiptGenerator = receiptGenerator;
        this.notificationService = notificationService;
    }

    public void process(Order order) {
        repository.save(order);
        byte[] receipt = receiptGenerator.generate(order);
        notificationService.notifyCustomer(order.getCustomerEmail(), receipt);
    }
}

Життєва аналогія

Конструктор LEGO проти шматка глини:

  • Код без SOLID: глиняна фігурка, запечена в печі. Якщо ви хочете прикріпити до фігурки крила або замінити руку, вам доведеться розбити її молотком і ліпити все наново.
  • Код із SOLID: модель із деталей LEGO. Кожен блок має стандартні кріплення (інтерфейси). Ви можете зняти колесо і поставити гусеницю за пару секунд, не розбираючи саму машинку.

🟡 Middle Level

Економічний сенс SOLID: TCO (Total Cost of Ownership)

У реальному бізнесі SOLID — це інструмент управління фінансовими витратами на розробку програмного забезпечення:

Вартість змін (TCO)
 │               /  (Проєкт без SOLID: лавиноподібне зростання витрат)
 │              /
 │             /
 │  ──────────/────── (Проєкт із SOLID: лінійна та передбачувана вартість)
 │ /
 └────────────────────────────► Час життя проєкту (Місяці/Роки)
  • Стартова фаза (0–3 місяці): Розробка з дотриманням SOLID вимагає на 15–25% більше часу на проєктування інтерфейсів, розділення класів та налаштування DI.
  • Фаза активної експлуатації (6+ місяців): У проєктах без SOLID час додавання кожної нової фічі експоненційно зростає через регресійні баги, страх зламати legacy-код та довгі злиття гілок. У SOLID-кодовій базі вартість змін залишається стабільною та передбачуваною.

SOLID у розподілених системах та мікросервісах

Принципи SOLID масштабуються з рівня Java-класів на рівень макроархітектури:

Принцип Рівень класів (OOP) Рівень мікросервісів та API
SRP Клас відповідає за одну бізнес-роль. Мікросервіс відповідає за один обмежений контекст (Bounded Context у DDD).
OCP Нова логіка підключається через патерн Strategy. Розширення API через нові версіоновані ендпоінти (v2/orders) або події Kafka без ламання старих клієнтів.
LSP Підклас задовольняє контракт предка. Зворотна сумісність API (Backward Compatibility) та дотримання Consumer-Driven Contracts (Pact).
ISP Дробіння «жирних» інтерфейсів на ролі. Патерн BFF (Backend for Frontend) або GraphQL — клієнти отримують тільки запитані поля.
DIP Залежність через Spring Constructor Injection. Сервіси залежать від стабільних контрактів схем (gRPC Protobuf, OpenAPI, AsyncAPI), а не від внутрішніх реалізацій один одного.

🔴 Senior Level

Метрики архітектурної якості: LCOM, Coupling та Instability

Для об’єктивного контролю дотримання принципів SOLID у Continuous Integration (CI) застосовуються статичні метрики:

1. LCOM (Lack of Cohesion of Methods — Чайдамбер і Кемерер)

Показує, наскільки методи класу взаємодіють зі спільними полями:

  • Якщо методи $M_1, M_2$ працюють з неперетинними групами полів $F_1$ та $F_2$, значення $LCOM$ прямує до 1.0.
  • $LCOM > 0.8$ у SonarQube — математичний доказ порушення SRP: клас розпався на незв’язані шматки і вимагає негайного розділення.

2. Метрики зв’язності пакетів Роберта Мартіна (SDP / SAP)

  • Afferent Coupling ($C_a$): кількість зовнішніх класів, які залежать від цього пакета (вхідні зв’язки).
  • Efferent Coupling ($C_e$): кількість зовнішніх класів, від яких залежить цей пакет (вихідні зв’язки).
  • Нестабільність ($I$): $I = \frac{C_e}{C_a + C_e}$.
    • $I = 0$ (максимально стабільний пакет) $\implies$ він зобов’язаний бути абстрактним (складатися з інтерфейсів за DIP).
    • $I = 1$ (максимально нестабільний пакет) $\implies$ у ньому розташовуються конкретні реалізації та адаптери.

Антипатерн “SOLID Cargo Cult” (Сліпе поклоніння)

Головна ознака Senior-інженера — розуміння меж застосовності принципів та захист від Over-Engineering:

  1. Interface Explosion (Вибух інтерфейсів): створення інтерфейсу IOrderService з рівно однією реалізацією OrderServiceImpl без реальної потреби в поліморфізмі чи динамічній заміні. Це порушення принципу YAGNI (You Aren’t Gonna Need It).
  2. Параліч абстракцій: 10 шарів маперів, DTO та адаптерів для тривіального CRUD сервісу. Кожна абстракція має ціну: cognitive load для нових розробників, оверхед на алокацію пам’яті та збирання сміття GC.
  3. KISS & CUPID: Альтернативні філософії розробки (наприклад, CUPID Дена Норта — Composable, Unix-like, Predictable, Idiomatic, Domain-based) фокусуються на радості читання коду та простоті, застерігаючи від перетворення SOLID на бюрократичний догматизм.

4 Tricky Questions

1. Чи можна вважати SOLID законами, порушення яких є безумовною помилкою?

Відповідь: Ні. SOLID — це евристичні рекомендації (heuristics), виведені емпіричним шляхом для зниження ризиків модифікації коду. Вони не є математичними аксіомами. Усвідомлений відступ від SOLID виправданий:

  • У Proof of Concept (PoC) та ранніх стадіях стартапів (де швидкість перевірки продуктової гіпотези важливіша за довгострокову підтримку).
  • У критичних ділянках продуктивності (Hot-Path у Low-Latency системах), де віртуальні виклики інтерфейсів, створення проміжних об’єктів для DIP/SRP та мегаморфний поліморфізм неприпустимі через затримки CPU та GC.
  • У простих CRUD застосунках із фіксованим життєвим циклом.

2. Який із принципів SOLID порушується найчастіше в реальних проєктах і чому?

Відповідь: Найчастіше порушується Single Responsibility Principle (SRP). Класи бізнес-сервісів (UserService, OrderService) мають тенденцію безперервно розростатися з часом. Розробникам психологічно простіше дописати ще один метод у наявний сервіс, ніж спроєктувати новий клас, впровадити його та зв’язати залежності. У результаті сервіси перетворюються на багатотисячні «божественні об’єкти» (God Objects), що концентрують у собі валідацію, транзакції, відправку сповіщень та мапінг DTO.


3. Як балансувати між DRY (Don’t Repeat Yourself) та принципами SOLID (зокрема SRP)?

Відповідь: Сліпе дотримання DRY часто руйнує SRP. Коли розробник бачить два однакових рядки коду в модулі авторизації та модулі білінгу, він виносить їх у спільний утилітарний метод. У результаті дві зовсім різні бізнес-ролі виявляються жорстко зв’язаними спільним кодом. Коли змінюються вимоги в білінгу, ламається авторизація. Senior керується правилом: «Випадкове дублювання (accidental duplication) коду набагато дешевше за неправильну абстракцію». Код об’єднується за DRY тільки тоді, коли він представляє одну й ту саму справжню бізнес-концепцію і змінюється з однієї й тієї самої причини.


4. У чому різниця між архітектурними дефектами Rigidity (Жорсткість) та Fragility (Крихкість)?

Відповідь:

  • Rigidity (Жорсткість): код чинить опір будь-яким змінам. Зміна вимагає колосальних трудовитрат, оскільки тягне за собою каскадне переписування довгого ланцюжка зв’язаних класів (порушення DIP та OCP).
  • Fragility (Крихкість): код легко змінити, але після зміни система ламається в найнесподіваніших і функціонально незв’язаних місцях (порушення SRP та LSP). Розробники бояться чіпати такий код, бо неможливо передбачити радіус ураження (blast radius).

🎯 Шпаргалка для інтерв’ю

Головні тези (30 секунд на відповідь):

  • Навіщо: Для зниження Total Cost of Ownership (TCO) та запобігання гниттю архітектури.
  • З чим бореться: З чотирма симптомами «гнилого коду» — Rigidity, Fragility, Immobility, Viscosity.
  • Економіка: SOLID окупається на горизонтах $> 6$ місяців, стабілізуючи вартість розробки нових фіч.
  • Баланс: Противаги догматизму SOLID — YAGNI, KISS та DRY. Абстракції мають створюватися за потребою, а не на випередження.

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

  • ❌ Називати SOLID суворими законами, що не мають винятків.
  • ❌ Стверджувати, що SOLID завжди робить програму швидшою (SOLID проєктує підтримуваність та масштабованість коду, а не мікросекундну продуктивність процесора).
  • ❌ Виправдовувати створення інтерфейсів з однією реалізацією принципом DIP там, де немає поліморфізму та планів розширення.

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