Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке принцип відкритості-закритості (OCP)?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Принцип відкритості-закритості (OCP)** - програмні сутності мають бути відкриті для розширення і закриті для модифікації. ```javascript class Shape { area() { throw new Error('Реалізуй'); } } class Circle extends Shape { area() { return Math.PI * this.r ** 2; } } // Новий тип? Новий клас. Існуючий код не чіпаємо. ``` **Головне:** розширення через наслідування або інтерфейси, без редагування протестованого коду.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Принцип відкритості-закритості (OCP)** - правило SOLID, яке каже: програмні сутності мають бути відкриті для розширення, але закриті для модифікації. ## Теорія ### TL;DR - OCP схожий на USB-порт: нові пристрої підключаєш без переробки комп'ютера - Головна ідея: нову поведінку додаєш новим кодом, а не редагуванням старого - Абстрактний клас або інтерфейс - стабільний контракт; нові типи реалізують його - Правило вибору: якщо нова функція вимагає змін у старих файлах, дизайн порушує OCP - Компроміс: поліморфний виклик додає ~2-10ns, що відчутно у гарячих циклах ### Швидкий приклад ```javascript // ПОГАНО: кожна нова фігура потребує редагування цього файлу class AreaCalculator { calculate(shape) { if (shape.type === 'circle') return Math.PI * shape.r ** 2; if (shape.type === 'square') return shape.side ** 2; // Прямокутник? Редагуй. Трикутник? Редагуй знову. } } // ДОБРЕ: нові фігури підключаються без змін у існуючому коді class Shape { area() { throw new Error('Реалізуй area()'); } } class Circle extends Shape { constructor(r) { super(); this.r = r; } area() { return Math.PI * this.r ** 2; } } class Rectangle extends Shape { constructor(w, h) { super(); this.w = w; this.h = h; } area() { return this.w * this.h; } } // new Rectangle(2, 3).area() → 6 // Shape і Circle: не торкалися. ``` Кожна нова фігура просто розширює `Shape`. Калькулятор викликає `.area()` на тому, що отримує, без жодного `if/else`. ### Абстракція vs реалізація OCP розділяє дві речі: **стабільний контракт** (абстрактний інтерфейс) і **розширювані деталі** (конкретні реалізації). Існуючий код лишається незмінним, бо нові типи підключаються до контракту через наслідування. Це захищає від регресійних помилок у вже протестованому коді. Зручна метафора з практики: `AreaCalculator` не повинен знати, які фігури існують. Він знає тільки одне: "у цієї штуки є метод `area()`". Контракт закрито. Нові фігури - відкриті. ### Коли застосовувати OCP OCP доречний, коли можна передбачити, що поведінка зростатиме в одному напрямку: - **Кілька форматів виведення** (логери, принтери, серіалізатори): базовий клас з `log()` або `serialize()`, нові формати - нові класи - **Системи плагінів**: визначаєш протокол або інтерфейс, треті сторони реалізують його без доступу до ядра - **Платіжні процесори**: Strategy pattern зі спільним інтерфейсом, Stripe або PayPal - окремий клас - **Стеки middleware**: `app.use()` в Express - це OCP на практиці Пропусти OCP для одноразового коду або невеликих утиліт, де витрати на абстракцію перевищують вигоду. Тут добре спрацьовує YAGNI. ### Як V8 обробляє поліморфні виклики JavaScript-рушії використовують **таблиці віртуальних методів (vtables)** для поліморфного диспетчеризування. Коли виконується `shape.area()`, V8 шукає в vtable конкретного класу і викликає потрібну реалізацію - без знання типу на етапі компіляції. Саме це робить OCP можливим у JavaScript. Вартість: 1-2 непрямі переходи на виклик, приблизно 2-10ns. На 10 000 фігур за один кадр це накопичується. У критичних за продуктивністю ділянках краще використовувати конкретні типи або пакетні операції. ### Типові помилки **Помилка 1: наслідування для стану замість поведінки** ```javascript // Неправильно: Square IS-A Rectangle ламає сеттери class Rectangle { constructor(w, h) { this.w = w; this.h = h; } setWidth(w) { this.w = w; } } class Square extends Rectangle { setWidth(w) { this.w = this.h = w; } // змінює базову поведінку } ``` `Square`, що перевизначає `setWidth`, ламає тих, хто використовує `Rectangle`. Це ще й порушує принцип підстановки Ліскова (LSP), що руйнує розширення OCP. Рішення: композиція. `class Square { constructor(s) { this.rect = new Rectangle(s, s); } }`. **Помилка 2: абстрактний клас з конкретними методами за замовчуванням** ```javascript // Неправильно: дефолтна логіка стає магнітом для змін class Shape { area() { return 0; } } // виглядає нешкідливо ``` Коли дефолтна логіка зміниться, всі підкласи під загрозою. Краще чисто абстрактний метод: кидай помилку, або в TypeScript оголошуй метод без тіла. **Помилка 3: передчасна абстракція** ```javascript // Неправильно: інтерфейс для кожної дрібниці // 50 реалізацій там, де вистачило б 3 функцій interface ILogger { log(): void; } ``` Починай з конкретного. Абстрагуй тільки тоді, коли справді потрібне розширення. **Помилка 4: поліморфізм у гарячих циклах** ```javascript // Ризиковано у критичних за продуктивністю ділянках shapes.forEach(shape => total += shape.area()); // vtable-пошук на кожен виклик ``` V8 іноді може девіртуалізувати це, але не завжди. Якщо профайлер показує цю ділянку як вузьке місце, використовуй типізовані масиви або конкретні типи. ### Де зустрічається в реальних проектах - **React**: HOC на кшталт `withRouter` у `react-router` розширюють компоненти без зміни їхнього коду - **Express**: middleware через `app.use()` додає авторизацію або логування без змін у роутері - **Redux**: `combineReducers` додає функціональність без редагування існуючих reducer-файлів - **NestJS**: декоратори `@Injectable` дозволяють розширювати модулі без змін у базових класах - **Stripe Node SDK**: класи PaymentMethod розширюють базу через плагіни, без редагування ядра ### Follow-up питання **Q:** Чим OCP відрізняється від принципу стабільних абстракцій (SAP)? **A:** OCP каже "не модифікуй". SAP кількісно визначає, наскільки абстрактним має бути пакет відносно кількості залежностей від нього. Роберт Мартін описує SAP як метрику здоров'я OCP через коефіцієнти fan-out/in. Відповідь джуніора: "це одне й те саме". Відповідь сеньйора: "SAP дає число, щоб виміряти, чи справді OCP працює". **Q:** Покажи порушення OCP і виправ його в TypeScript через інтерфейси. **A:** Порушення: функція зі `switch` по рядкових літералах. Виправлення: `interface Shape { area(): number; }`, один клас на тип фігури. Функція, що приймає `Shape`, більше ніколи не змінюється. **Q:** Які компроміси OCP в мікросервісах порівняно з монолітом? **A:** У моноліті наслідування і спільні базові класи прості у використанні. У мікросервісах спільні базові класи стають проблемою зв'язування між сервісами. Там краще підходять event-контракти або OpenAPI-схеми як "закрита" частина, а розширення відбуваються в окремих сервісах. **Q:** У легасі-кодовій базі зі 100 гілками `if/else`, який твій план міграції до OCP? **A:** Патерн Strangler. Спочатку виділяєш інтерфейс, обгортаєш легасі-код в адаптер, мігруєш по одній гілці за раз. Тести перед кожним кроком. Жодних великих рефакторингів за один раз. ## Приклади ### Базовий: калькулятор площі фігур ```javascript class Shape { area() { throw new Error('Підклас має реалізувати area()'); } } class Circle extends Shape { constructor(radius) { super(); this.radius = radius; } area() { return Math.PI * this.radius ** 2; } } class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; } area() { return this.width * this.height; } } class Triangle extends Shape { constructor(base, height) { super(); this.base = base; this.height = height; } area() { return (this.base * this.height) / 2; } } const shapes = [new Circle(5), new Rectangle(4, 6), new Triangle(3, 8)]; shapes.forEach(s => console.log(s.area())); // 78.54, 24, 12 ``` Додавання `Triangle` не торкнулося жодного рядка в `Shape`, `Circle` або `Rectangle`. Новий клас - і все. ### Середній рівень: Express middleware для логування ```javascript // Базовий логер - закритий для модифікації class Logger { log(request) { console.log(`${request.method} ${request.url}`); } } // Розширення для JSON-формату - Logger не чіпаємо class JSONLogger extends Logger { log(request) { console.log(JSON.stringify({ method: request.method, url: request.url, timestamp: new Date().toISOString() })); } } // Розширення для Sentry - Logger знову не чіпаємо class SentryLogger extends Logger { log(request) { Sentry.captureMessage(`Access: ${request.method} ${request.url}`); } } const logger = new JSONLogger(); app.use((req, res, next) => { logger.log(req); next(); }); // Вивід: {"method":"GET","url":"/api/users","timestamp":"2025-01-15T10:00:00Z"} ``` Потрібен `DatadogLogger` завтра? Один новий клас. `Logger`, `app.js` і `JSONLogger` лишаються незмінними. ### Просунутий рівень: React HOC-композиція ```javascript // Закритий HOC-контракт - надає prop featureEnabled function withFeature(WrappedComponent) { return class extends React.Component { render() { return <WrappedComponent {...this.props} featureEnabled={true} />; } }; } // Розширення: додає аналітику - WrappedComponent не торкаємось function withAnalytics(WrappedComponent) { return class extends React.Component { componentDidMount() { analytics.track('ComponentMounted', this.props); } render() { return <WrappedComponent {...this.props} />; } }; } // Композиція без змін у Button const TrackedButton = withAnalytics(withFeature(Button)); // Монтує Button, логує {page: 'home', featureEnabled: true} ``` Порядок HOC має значення. `withAnalytics` обгортає `withFeature(Button)`, тому аналітика спрацьовує після того, як `featureEnabled` вже є в props. Поміняй порядок - і `featureEnabled` зникне з payload аналітики. Це і є підступний момент у OCP-композиції HOC, який виловлюють на співбесідах рівня сеньйор.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.