Чи можна слідувати всім принципам SOLID одночасно
SOLID — це не математичні закони і не юридичний кодекс. Це набір інженерних евристик і рекомендацій, покликаних знижувати вартість підтримки складного коду.
🟢 Junior Level
Коротка відповідь: Теоретично — так, але на практиці слідувати всім п’яти принципам абсолютно скрізь не лише не потрібно, але й шкідливо.
SOLID — це не математичні закони і не юридичний кодекс. Це набір інженерних евристик і рекомендацій, покликаних знижувати вартість підтримки складного коду.
Спроба застосувати всі 5 принципів до кожного класу в проєкті перетворює просту програму на нечитабельний лабіринт із сотень інтерфейсів, фабрик та адаптерів.
Життєва аналогія
Будівництво собачої будки:
- Якщо ви будуєте 50-поверховий хмарочос, вам необхідні геологічні вишукування, сейсмостійкий фундамент, незалежні пожежні відсіки та дублюючі ліфтові шахти (повний SOLID).
- Якщо ви будуєте дерев’яну будку для собаки на подвір’ї, використання тих самих норм призведе до того, що ви витратите
$100 000і 6 місяців на проєкт будки з автономною системою вентиляції та ліфтом. Будка повинна бути просто надійною і теплою.
Приклад шкідливого фанатизму (Over-engineering)
Абсурдне слідування SOLID для тривіальної операції:
// 5 файлів заради додавання двох чисел:
public interface AdditionOperation {
int execute(int a, int b);
}
public class IntegerAdditionStrategy implements AdditionOperation {
@Override
public int execute(int a, int b) { return a + b; }
}
public class CalculatorService {
private final AdditionOperation addition;
public CalculatorService(AdditionOperation addition) { this.addition = addition; }
public int add(int a, int b) { return addition.execute(a, b); }
}
Здоровий інженерний підхід (KISS):
public class Calculator {
public static int add(int a, int b) {
return a + b;
}
}
🟡 Middle Level
Внутрішні суперечності та конфлікти з іншими принципами
Прагнення дотриматися всіх принципів SOLID одночасно наштовхується на фундаментальні архітектурні суперечності:
1. SOLID проти KISS (Keep It Simple, Stupid) та YAGNI
- Конфлікт: Принцип OCP вимагає створювати точки розширення (інтерфейси, фабрики, реєстри стратегій). Але принцип YAGNI (You Aren’t Gonna Need It) застерігає: «Не пиши код для сценаріїв, які тобі зараз не потрібні».
- У 80% випадків гіпотетичне «розширення в майбутньому» ніколи не відбувається, а проєкт залишається обтяженим зайвими шарами непрямості.
2. SRP та ISP проти Зв’язності (Coupling)
- Принцип SRP вимагає ділити класи на мінімальні відповідальності, а ISP — дробити інтерфейси на мінімальні ролі.
- Доведення цього до фанатизму призводить до антипатерна «Атомарний пил» (Atomic Dust): сотні мікрокласів по 5 рядків. Щоб зрозуміти сценарій оформлення замовлення, розробник змушений тримати відкритими 25 вкладок в IDE. Внутрішня зв’язність модулів (Coupling) стає катастрофічною.
3. SOLID проти Продуктивності (Low-Latency / Highload)
- Кожен поділ за SRP вимагає створення додаткового об’єкта в купі (Heap), збільшуючи навантаження на Garbage Collector.
- Кожен виклик через інтерфейс за DIP/OCP — це
invokeinterfaceта потенційний мегаморфний диспатчинг, що блокує JIT-інлайнінг компілятора C2.
Градієнтна стратегія застосування SOLID
Досвідчена команда застосовує принципи SOLID не бінарно («все або нічого»), а градієнтно, залежно від зрілості та призначення модуля:
| Шар / Етап системи | Рівень суворості SOLID | Пріоритетні принципи | Обґрунтування |
|---|---|---|---|
| Прототипи, Spikes, MVP | Мінімальний | KISS, DRY | Головна мета — перевірити гіпотезу за 3 дні, а не спроєктувати вічний фундамент. |
| Інфраструктурні утиліти | Низький | Статичні методи (StringUtils) |
Стабільні алгоритми без стану, які ніколи не будуть поліморфно підмінятися. |
| Hot-Path (Highload ядро) | Обмежений | Процедурна швидкість | Усунення віртуальних викликів та алокацій пам’яті заради Latency в мікросекундах. |
| Core Domain (Бізнес-логіка) | Високий | Усі принципи SOLID | Найдорожча частина системи, що живе роками і постійно змінюється під тиском бізнесу. |
🔴 Senior Level
Апаратна ціна абстракцій на рівні JVM
Senior-інженер чітко усвідомлює фізичну ціну «чистої архітектури» під навантаженням:
Бенчмарк для обробки 10 000 000 транзакцій (Hotspot C2):
┌──────────────────────────────┬──────────────────┬─────────────────┬────────────────┐
│ Варіант реалізації │ Latency (p99) │ Алокація Heap │ GC Pauses │
├──────────────────────────────┼──────────────────┼─────────────────┼────────────────┤
│ Прямий процедурний виклик │ 1.2 ns │ 0 байтів │ 0 ms │
│ SOLID (Інтерфейс + Стратегія)│ 8.5 ns (+600%) │ 24 байти / викл │ 120 ms (Young) │
└──────────────────────────────┴──────────────────┴─────────────────┴────────────────┘
- VTable / ITable Overhead: Якщо інтерфейс OCP має понад дві реалізації в рантаймі, точка виклику стає мегаморфною. JIT-компілятор відключає Monomorphic Inline Caching, виконуючи непрямий перехід за вказівником у таблиці інтерфейсів зі скиданням передбачувача переходів процесора (Branch Misprediction).
- Object Header Padding: Розділення монолітного об’єкта на 5 дрібних класів за SRP створює 5 заголовків об’єктів (Object Headers) по 12–16 байтів кожен + вирівнювання до 8 байтів (Padding). При 100 000 RPS це означає гігабайти сміття на секунду в поколінні Eden.
Архітектурний компроміс: Expression Problem та Sealed Hierarchies
У Java 21 поява Sealed Interfaces та Pattern Matching for switch остаточно зруйнувала міф про те, що потрібно завжди слідувати класичному OCP:
- Поліморфізм OCP (Інтерфейси + Реалізації): відкритий для додавання нових типів даних, але закритий і незручний для додавання нових операцій (потрібно змінювати методи в усіх 20 класах).
- Sealed Types (Функціональна декомпозиція): закритий для додавання нових типів даних (список
permitsсуворо фіксований), але ідеально відкритий для додавання нових операцій через зовнішнійswitch.
Висновок для Senior: Спроба слідувати OCP у закритих предметних областях (наприклад, статусна модель замовлення:
NEW,PAID,CANCELLED) — це помилка проєктування. Тутsealed interfaceзіswitchперевершує класичний SOLID за типобезпекою та виразністю.
4 Tricky Questions
1. Як конфлікт між OCP та YAGNI вирішується на практиці за допомогою «Правила трьох» (Rule of Three)?
Відповідь: «Правило трьох» стверджує:
- Коли ви пишете код уперше — зробіть його максимально простим (KISS, без абстракцій).
- Коли аналогічна логіка знадобилася вдруге — допустіть дублювання, але зверніть увагу на схожість.
- Тільки коли та сама варіативність виникає втретє — проводьте рефакторинг до OCP (виділяйте інтерфейс, стратегію або фабрику). Це охороняє проєкт від спекулятивної генералізації (Speculative Generality) та створення десятків порожніх інтерфейсів з єдиною реалізацією.
2. Чи є створення інтерфейсу з суфіксом Impl (UserService -> UserServiceImpl) дотриманням SOLID?
Відповідь:
У 90% випадків це антипатерн механічного карго-культу (Header Interface), а не дотримання SOLID.
Якщо в інтерфейсу існує рівно одна реалізація і клас не є публічним API сторонньої бібліотеки, інтерфейс не дає жодної поліморфної гнучкості (OCP) і не інвертує залежності (DIP), а лише подвоює кількість файлів у проєкті.
Інтерфейс повинен створюватися тільки тоді, коли є щонайменше дві різні реалізації (наприклад, PostgresUserRepository та InMemoryUserRepository), або коли необхідно приховати внутрішню реалізацію за межею архітектурного модуля.
3. Чи можна порушувати SOLID у JPA Entity-класах і чому?
Відповідь: Так, у JPA/Hibernate сутностях принципи SOLID порушуються усвідомлено:
- Сутність бази даних об’єднує в собі схему таблиці (поля), бізнес-валідацію, методи життєвого циклу (
@PrePersist) та анотації мапінгу (порушення SRP). - Сутності містять публічні гетери та сетери для рефлексії Hibernate (порушення інкапсуляції). Спроба декомпозувати JPA Entity на дрібні класи за суворим SRP руйнує ORM-мапінг і призводить до катастрофічного падіння продуктивності SQL-запитів (проблема N+1, надлишкові JOIN). Інженери ізолюють це порушення всередині шару доступу до даних (Data Access Layer), не випускаючи сутності JPA в доменну бізнес-логіку.
4. Що таке «Архітектура, керована болем» (Pain-Driven Development) у контексті застосування SOLID?
Відповідь: Це прагматична інженерна філософія, згідно з якою рефакторинг коду в бік SOLID проводиться не превентивно, а лише тоді, коли поточна структура починає завдавати реального болю команді:
- Стало боляче писати юніт-тести $\implies$ впроваджуємо DIP (інтерфейси та DI).
- Кожна зміна ламає непов’язані модулі $\implies$ розпилюємо клас за SRP.
- Різні гілки
if-elseпостійно конфліктують у Git $\implies$ виносимо логіку в стратегії за OCP. Якщо код працює, покритий тестами і не змінюється роками — слідувати SOLID для нього не має жодного економічного сенсу.
🎯 Шпаргалка для інтерв’ю
Головні тези (30 секунд на відповідь):
- Головна теза: Слідувати всім принципам SOLID абсолютно скрізь неможливо і контрпродуктивно (ризик Over-engineering та порушення KISS/YAGNI).
- SOLID — це trade-off: Кожна абстракція підвищує розширюваність, але погіршує читабельність коду, продуктивність процесора (віртуальні виклики) та збільшує витрату пам’яті.
- Де SOLID обов’язковий: Ядро складної бізнес-логіки (Core Domain), довгоживучі Enterprise-модулі, публічні бібліотеки.
- Де SOLID шкідливий: Прототипи/MVP, одноразові утиліти, Hot-Path ділянки з високими вимогами до затримок (Ultra-Low Latency).
- Альтернатива OCP: У Java 21
sealed-ієрархії зіswitchкращі за поліморфізм OCP для закритих наборів типів.
Чого уникати (Red Flags):
- ❌ Стверджувати: «Я завжди і скрізь на 100% дотримуюся всіх принципів SOLID» (видає догматика без досвіду комерційних компромісів).
- ❌ Заявляти, що абстракції та інтерфейси нічого не коштують з точки зору JVM (пам’ять, GC, деоптимізація C2).
- ❌ Створювати пари
Interface+InterfaceImplдля кожного класу в проєкті.