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

Чи можна слідувати всім принципам 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) │
└──────────────────────────────┴──────────────────┴─────────────────┴────────────────┘
  1. VTable / ITable Overhead: Якщо інтерфейс OCP має понад дві реалізації в рантаймі, точка виклику стає мегаморфною. JIT-компілятор відключає Monomorphic Inline Caching, виконуючи непрямий перехід за вказівником у таблиці інтерфейсів зі скиданням передбачувача переходів процесора (Branch Misprediction).
  2. 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)?

Відповідь: «Правило трьох» стверджує:

  1. Коли ви пишете код уперше — зробіть його максимально простим (KISS, без абстракцій).
  2. Коли аналогічна логіка знадобилася вдруге — допустіть дублювання, але зверніть увагу на схожість.
  3. Тільки коли та сама варіативність виникає втретє — проводьте рефакторинг до 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 для кожного класу в проєкті.

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