Підрозділ 20.1
Вступ до SOLID. Проблеми без архітектурних принципів
20.1. Вступ до SOLID. Проблеми без архітектурних принципів SOLID — це акронім із п'яти принципів об'єктно орієнтованого проектування, сформульованих Робертом Мартіном Robert C. Martin, відомий у спільноті як «U
20.1. Вступ до SOLID. Проблеми без архітектурних принципів
SOLID — це акронім із п'яти принципів об'єктно-орієнтованого проектування, сформульованих Робертом Мартіном (Robert C. Martin, відомий у спільноті як «Uncle Bob») у статтях кінця 1990-х і систематизованих у книзі «Agile Software Development: Principles, Patterns, and Practices» (2002). Сам акронім склав Майкл Фезерс (Michael Feathers) у 2004 році. За понад двадцять років ці принципи стали індустріальним стандартом — вони описують, якою має бути архітектура коду, що призначений жити довго і змінюватися без катастрофічних наслідків.
Важливо розуміти з самого початку: SOLID — це не суворі правила, яким треба сліпо слідувати, і не алгоритм із єдиною правильною відповіддю. Це набір евристик — практичних настанов, що виникли з колосального досвіду супроводу великих систем. Кожен принцип відповідає на конкретне питання: «Яка властивість поганого коду робить його болючим для зміни, і як від неї позбутися?»
Що таке «хороший» і «поганий» код
Студенти-початківці часто оцінюють код за критерієм «працює / не працює». Але в реальних проектах набагато важливіший інший критерій: як легко цей код змінити. Програмне забезпечення за означенням є «м'яким» (software — soft + ware), тобто таким, що має змінюватися. Вимоги змінюються, бізнес змінюється, закони змінюються, технології змінюються. Код, який неможливо змінити без болю, — це код, який не виконує свого призначення.
Роберт Мартін описував симптоми «поганого» дизайну чотирма поняттями:
Жорсткість (Rigidity) — код важко змінити, бо будь-яка зміна тягне за собою каскад інших змін. Виправив один клас — зламалось п'ять. Щоб додати нову функцію, доводиться переписувати існуючий код.
Крихкість (Fragility) — зміна в одному місці несподівано ламає зовсім не пов'язані частини системи. Відремонтував звіти — перестали надсилатися email. Ніхто не може пояснити, чому. З часом команда боїться чіпати код взагалі.
Нерухомість (Immobility) — неможливо перевикористати компонент в іншому проекті, бо він занадто щільно сплетений з рештою системи. Хочеш взяти «тільки логіку бронювання» — разом з нею тягнеться база даних, email-відправник і PDF-генератор.
В'язкість (Viscosity) — правильне рішення важче реалізувати, ніж неправильне. Щоб зробити «як треба», потрібно переписати багато; щоб зробити «як-небудь» — достатньо одного рядка. Розробники обирають швидкий шлях, і система деградує.
Ці чотири симптоми — не окремі проблеми, а прояви одного й того ж явища: надмірної зв'язаності (tight coupling) і недостатньої зв'язності (low cohesion). SOLID — це систематизований спосіб досягти протилежного: слабкої зв'язаності і сильної зв'язності.
Технічний борг: ціна «швидких» рішень
Кожне рішення, що спрощує код зараз, але ускладнює його зміну в майбутньому, — це технічний борг. Як фінансовий борг, він накопичує «відсотки»: чим довше він існує, тим більше зусиль потрібно для кожної наступної зміни. Системи, де технічний борг накопичувався роками, перетворюються на «вавилонські вежі» — конструкції, в яких ніхто не розуміє, що відбувається, і всі бояться щось чіпати.
Характерний приклад: розробник поспішав, додав логіку відправки email прямо у клас збереження пацієнта — «тимчасово». Через рік клас збереження пацієнта не можна протестувати без SMTP-сервера. Через два роки ніхто вже не пам'ятає, чому email там взагалі є.
God Class: коли один клас знає все
Найпоширеніший прояв жорсткості й крихкості — «God Class» (клас-Бог): клас, що бере на себе надто багато відповідальностей. Він «знає» все про всіх підсистемах і «робить» все сам. Поява такого класу — майже завжди наслідок того, що функціонал «по-швидкому» додавали в одне місце замість того, щоб проектувати архітектуру.
Ось як виглядає типова еволюція клінічної системи без принципів:
Цей клас технічно працює. Але подивимося, що відбувається, коли вимоги змінюються:
- Замовник хоче надсилати SMS замість email → треба змінити
ClinicManager - Потрібно зберігати у базу даних замість файлу → треба змінити
ClinicManager - Нова формула рахунків → треба змінити
ClinicManager - Хочемо написати тест на бронювання → неможливо без SMTP-сервера і файлової системи
- Хочемо перевикористати логіку бронювання в іншому модулі → разом з нею тягнеться весь
ClinicManager
Клас має шість різних причин для зміни — і будь-яка зміна несе ризик зламати всі інші шість частин.

Каскад змін: як один рядок ламає систему
Розглянемо конкретну ситуацію: потрібно змінити формат рядка у звіті. Здавалося б, незначна правка.
Це і є жорсткість: зміна одного рішення (формату рядка) потребує пошуку і виправлення всіх місць, де це рішення «вшите». У великих кодових базах розробники часто не знають про всі такі місця — звідси й крихкість: щось ламається там, де не очікували.

П'ять принципів: короткий огляд
SOLID — це п'ять принципів, кожен із яких «лікує» один конкретний симптом поганого дизайну:

S — Single Responsibility Principle (SRP) Принцип єдиної відповідальності: клас має мати лише одну причину для зміни. «Причина для зміни» — це конкретна роль у системі. Клас, що одночасно керує даними, надсилає повідомлення і генерує звіти, має три причини для зміни. SRP вимагає розбити його на три окремі класи.
O — Open/Closed Principle (OCP) Принцип відкритості/закритості: програмні сутності мають бути відкриті для розширення, але закриті для модифікації. Додавання нової поведінки не повинно вимагати зміни існуючого, вже перевіреного коду. Нова поведінка реалізується через розширення (нові класи, нові реалізації інтерфейсів), а не через правку старого.
L — Liskov Substitution Principle (LSP) Принцип підстановки Ліскова (Barbarа Liskov, 1987): об'єкт підкласу має бути взаємозамінним з об'єктом базового класу без порушення коректності програми. Якщо замінити базовий клас підкласом — жодна частина системи не повинна «здивуватися». Це означає, що ієрархія успадкування має відображати справжнє відношення «є» (is-a), а не зручність повторного використання коду.
I — Interface Segregation Principle (ISP) Принцип розподілу інтерфейсів: клієнти не повинні залежати від методів, яких вони не використовують. Великий інтерфейс, що об'єднує непов'язані операції, змушує кожного реалізатора брати на себе зайві зобов'язання. ISP вимагає ділити великі інтерфейси на вузькоспеціалізовані.
D — Dependency Inversion Principle (DIP) Принцип інверсії залежностей: модулі верхнього рівня не повинні залежати від модулів нижнього рівня — обидва мають залежати від абстракцій. Абстракції не повинні залежати від деталей — деталі мають залежати від абстракцій. На практиці це означає: бізнес-логіка не знає, що конкретно зберігає дані (файл? БД? пам'ять?) — вона знає лише інтерфейс сховища.
Чому SOLID вивчають саме після ООП
Принципи SOLID мають сенс лише тоді, коли вже відомі: класи, об'єкти, успадкування, поліморфізм, інтерфейси, узагальнення. Саме тому вони стоять наприкінці курсу — це не введення у нові синтаксичні конструкції, а синтез усього вивченого в єдиний спосіб мислення про архітектуру.
Кожен принцип вирішує проблему, яку студент вже міг зустріти під час виконання попередніх лаб:
- SRP вирішує проблему «God Class», який ріс з кожною лабою
- OCP вирішує проблему «треба додати новий тип прийому, але не хочеться чіпати AppointmentManager»
- LSP вирішує проблему «підклас поводиться не як базовий клас»
- ISP вирішує проблему «мусив реалізувати методи, що не потрібні»
- DIP вирішує проблему «неможливо протестувати PatientManager без бази даних»
Наступні п'ять секцій розбирають кожен принцип детально — з прикладами порушення, поясненням суті і рефакторингом на конкретному коді клінічної системи.