Навіщо взагалі потрібні принципи 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:
- Rigidity (Жорсткість): систему важко змінювати. Спроба змінити один рядок коду тягне за собою каскад правок у десятках інших залежних класів.
- Fragility (Крихкість): зміна в одному модулі несподівано ламає логіку в зовсім непов’язаній частині програми, про яку розробник навіть не підозрював.
- Immobility (Нерухомість): модуль неможливо перевикористати в іншому сервісі чи проєкті, бо він намертво «приріс» до конкретної бази даних або контексту.
- 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:
- Interface Explosion (Вибух інтерфейсів): створення інтерфейсу
IOrderServiceз рівно однією реалізацієюOrderServiceImplбез реальної потреби в поліморфізмі чи динамічній заміні. Це порушення принципу YAGNI (You Aren’t Gonna Need It). - Параліч абстракцій: 10 шарів маперів, DTO та адаптерів для тривіального CRUD сервісу. Кожна абстракція має ціну: cognitive load для нових розробників, оверхед на алокацію пам’яті та збирання сміття GC.
- 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 там, де немає поліморфізму та планів розширення.