Все, що ви хотіли знати про принципи SOLID. Частина друга: OCP
Вітаю, друзі! З вами знову Сергій Немчинський — програміст з понад
У мене є проста, але амбітна мрія — зробити світ програмування кращим через збільшення кількості професійних розробників, які пишуть якісний код. Саме тому я працюю над серією статей про принципи SOLID, їхню логіку, цінність та вклад в створення чистого, підтримуваного коду.
Так, я чудово розумію, що і концепція «чистого коду», і принципи SOLID часто стають об’єктами суворої критики. Однак я переконаний: перш ніж критикувати будь-яку методологію, варто її ґрунтовно вивчити. Лише так можна сформувати об’єктивне професійне судження.
Тож запрошую вас до другої статті циклу, присвяченої принципу відкритості-закритості (Open-Closed Principle).
Контекст (що було в попередній серії)
Для тих, хто пропустив першу статтю, нагадаю: принципи SOLID були зібрані й систематизовані Робертом Мартіном. Ми вже розглянули принцип єдиної відповідальності (Single Responsibility Principle) — літеру «S» в акронімі SOLID. Сьогодні ж зосередимося на другому принципі — «O», Open-Closed Principle, або принцип відкритості-закритості.
Принцип відкритості-закритості (Open-Closed Principle)
Принцип відкритості-закритості (OCP) був сформульований Бертраном Мейєром у 1988 році в книзі «Object-Oriented Software Construction». Власне, крім цього Бертран Мейєр мало чим відомий, тож на його персоналії ми не зупинятимемося. Пізніше Роберт Мартін інтегрував цей принцип до своєї колекції SOLID-принципів.
Нагадаю, що принципи SOLID не є революційними концепціями, створеними з нуля. Це кристалізація практичного досвіду багатьох поколінь інженерів-програмістів. Яка ж практична проблема стоїть за створенням принципу відкритості-закритості? Дуже банальна: вартість розробки та оновлення софта!
Будь-яка успішна програма живе довше першої версії. З часом додається новий функціонал. Якщо при встановленні оновлень змінюється існуючий код, вся система потребує регресійного тестування. А це шалені гроші та час.
Щоб здешевити вартість розробки, треба вирішити дилему: як додавати новий функціонал до існуючої системи таким чином, щоб не порушити її стабільність і не витрачати дорогоцінні ресурси на повторне тестування? OCP пропонує елегантне рішення.
Сутність принципу OCP
Формальне визначення принципу звучить так:
Програмні сутності (класи, модулі, функції тощо) повинні бути відкриті для розширення, але закриті для модифікації.
Розшифровуємо, що це означає людською, зрозумілою мовою.
Відкритість для розширення означає, що архітектура програмної системи має бути спроєктована так, щоб додавання нових можливостей не вимагало перебудови існуючої структури. Система повинна легко «приймати» нові компоненти.
Закритість для модифікації означає, що вже написаний, протестований і впроваджений код не повинен змінюватися при додаванні нової функціональності. «Працює? Не чіпай!» Не варто ризикувати стабільністю системи, вносячи зміни до перевіреного коду.
На перший погляд ці дві вимоги здаються взаємовиключними. Як можна додавати нові можливості, не змінюючи існуючий код? Сутність принципу OCP полягає саме в розв’язанні цього протиріччя через правильне архітектурне рішення.
Два підходи до дотримання принципу
Існують різні підходи до реалізації принципу відкритості-закритості, кожен зі своїми перевагами та особливостями застосування.
Підхід Бертрана Мейєра Бертран Мейєр, який спочатку запропонував цей принцип, передбачав наступну реалізацію. Вже написаний код «закривається» — у ньому більше нічого не змінюється, окрім виправлення помилок. Розширення функціональності відбувається через наслідування: ви створюєте новий клас, який наслідує базовий, і додаєте в нього нову функціональність.
Особливість підходу Мейєра полягає в тому, що інтерфейс нового класу може відрізнятися від інтерфейсу базового класу. Це дещо суперечлива ідея, оскільки код, який використовував базовий клас, тепер має бути адаптований для роботи з новим інтерфейсом. Тобто ви все одно змінюєте код, який взаємодіє з класом, хоча сам базовий клас не змінюєте.
Поліморфний підхід Роберта Мартіна до принципу відкритості-закритості ґрунтується на простій ідеї: код, що використовує певний функціонал, повинен залежати від абстракцій, а не від конкретних реалізацій.
Мартін пропонує працювати у зворотному напрямку порівняно з підходом Мейєра. Замість того, щоб створювати нові реалізації зі зміненими інтерфейсами, ми спочатку проєктуємо стабільний інтерфейс, який не змінюватиметься з часом, а потім створюємо різні реалізації цього інтерфейсу.
Цей підхід має два ключових аспекти:
- Стабільність інтерфейсу. Інтерфейс, через який відбувається взаємодія з функціональністю, залишається незмінним. Це гарантує, що код, який використовує цю функціональність, не потребуватиме змін при розширенні системи.
- Розширення через нові реалізації. Нова функціональність додається шляхом створення нових класів, які реалізують той самий інтерфейс. Це може відбуватися через механізми наслідування або делегування.
Завдяки цьому підходу клієнтський код не потрібно змінювати при додаванні нових функціональних можливостей. Коли з’являється потреба в новій поведінці, ми просто створюємо новий клас, що реалізує вже існуючий інтерфейс, і використовуємо його замість попередньої реалізації. Такий підхід значно практичніший, ніж початкова ідея Мейєра.
Практичне застосування OCP
Приклад принципу Open-Closed. В минулій статті ми згадували розробку Embedded-системи, в якій є датчик, що вимірює час та температуру. Уявимо клас SensorMonitor, що відповідає за зчитування, обробку та відображення даних з датчика. У класі є метод для обробки даних, який перевіряє тип даних і відповідно обробляє.
Все працює чудово, доки до вас не приходить колега Василь і каже: «У нас з’явився новий датчик вологості, можеш додати його обробку в SensorMonitor?» Ви відкриваєте вже існуючий і протестований клас, додаєте новий блок коду для датчика вологості, змінюєте логіку і проводите повторне тестування.
Через тиждень приходить інший колега, Петро, і просить: «Нам потрібно додати підтримку датчика тиску, можеш оновити SensorMonitor?» І знову вам доводиться змінювати вже існуючий клас, додавати ще один блок коду, змінювати логіку і заново тестувати.
Кожного разу, коли з’являється новий тип датчика, ви змушені змінювати той самий клас. Це порушує принцип відкритості-закритості, бо клас SensorMonitor не відкритий для розширення, але відкритий для модифікації.
Розв’язання проблеми згідно з OCP. Ви переробляєте систему, використовуючи інтерфейси та поліморфізм. Створюєте інтерфейс SensorHandler з методом для обробки даних датчика, і окремі класи для кожного типу датчика, які реалізують цей інтерфейс.
Ваш SensorMonitor тепер зберігає набір обробників для різних типів датчиків і делегує обробку даних відповідному обробнику, а не обробляє дані сам.
Тепер, коли Василь приходить з датчиком вологості, ви не змінюєте існуючий код, а створюєте новий клас для обробки даних вологості, який реалізує інтерфейс SensorHandler.
Коли пізніше приходить Петро з датчиком тиску, відбувається те саме — створюється новий клас-обробник без змін існуючого коду.
Клас SensorMonitor відкритий для розширення (можна додавати нові типи датчиків), але закритий для модифікації (не потрібно змінювати існуючий код).
Це ідеальний приклад принципу Open-Closed: розширення функціональності системи без модифікації вже написаного, протестованого та впровадженого коду.
Застосування OCP для різних архітектурних патернів
Розглянутий приклад демонструє лише один зі способів застосування принципу відкритості-закритості. На практиці цей принцип можна реалізувати за допомогою різних архітектурних підходів і патернів проєктування. Розглянемо, як поліморфний підхід Роберта Мартіна відкриває широкі можливості для гнучкої архітектури.
1. Проксі. Створюється клас, який реалізує той самий інтерфейс і делегує роботу серверу.
2. Декоратор. Працює так само як і проксі, але додає додаткову функціональність.
Завдяки такому підходу ми можемо вставити між клієнтом і сервером будь-яку кількість додаткового функціоналу: логування, кешування, автентифікацію, авторизацію, аудит і так далі.
Пошук балансу між жорстким та гнучким програмуванням
Програміст постійно стикається з фундаментальною дилемою. З одного боку, існує спокуса швидкого рішення — hard-code підхід, коли всі залежності та зв’язки «прибиваються цвяхами» прямо в коді. Цей метод дозволяє швидко отримати результат, але неминуче призводить до проблем при необхідності масштабування чи модифікації системи.
З іншого боку знаходиться soft-code — класичний антипатерн, коли компоненти системи взаємодіють через надмірно абстраговані механізми: фабрики, dependency injection та інші складні інструменти. Такий підхід зрештою створює систему, яку надзвичайно складно конфігурувати, розробляти та підтримувати.
Моє бачення полягає в тому, що досвідчений програміст завжди шукає золоту середину між цими крайнощами, а середина якраз і полягає в OCP. На мій погляд, оптимальне рішення зазвичай знаходиться ближче до принципів Inversion of Control (IoC) чи Dependency Inversion — використання всіх класів через чітко визначені інтерфейси. Я переконаний, що цей підхід забезпечує необхідну гнучкість, зберігаючи при цьому простоту та зрозумілість архітектури, і ми в такому випадку можемо додати використання проксі чи декоратора, як я вже показав вище.
Іноді розробники настільки захоплюються створенням розширюваної архітектури, що система стає надмірно складною й важкою для розуміння. Якщо абстракція створює більше проблем, ніж вирішує, варто переглянути підхід і не використовувати ОСР. Як казав Альберт Ейнштейн:
Все має бути зроблено настільки просто, наскільки це можливо, але не простіше
Пам’ятайте, що принципи SOLID, включно з OCP, є дороговказами, а не догмами. Хороший розробник розуміє, коли суворо дотримуватися принципів, а коли допустимо зробити компроміс заради інших важливих якостей коду, таких як простота та зрозумілість.
8 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів