Чи використовуєте ви переваги принципу відкритого закриття?


12

Принцип відкритого закриття (OCP) говорить, що об'єкт повинен бути відкритим для розширення, але закритим для модифікації. Я вважаю, що я це розумію і використовую разом із SRP для створення класів, які виконують лише одне. І я намагаюся створити багато маленьких методів, які дозволять витягти всі елементи контролю поведінки в методи, які можуть бути розширені або переопрацьовані в якомусь підкласі. Таким чином, я закінчую класами, які мають багато точок розширення, будь то через: введення залежності та склад, події, делегування тощо.

Розглянемо наступний простий клас, що розширюється:

class PaycheckCalculator {
    // ...
    protected decimal GetOvertimeFactor() { return 2.0M; }
}

Тепер скажіть, наприклад, що OvertimeFactorзміни до 1,5. Оскільки вищевказаний клас був розроблений для розширення, я можу легко підкласирувати та повертати інший OvertimeFactor.

Але ... незважаючи на те, що клас призначений для розширення та дотримання OCP, я зміню єдиний розглянутий метод, а не підкласифікувати та переосмислити відповідний метод, а потім повторно підключити мої об'єкти до свого контейнера IoC.

У результаті я порушив частину того, що намагається здійснити OCP. Таке відчуття, що я просто лінуюся, бо вище сказане трохи простіше. Я нерозумію OCP? Чи справді я повинен робити щось інше? Чи використовуєте ви переваги OCP по-різному?

Оновлення : виходячи з відповідей, схоже, цей надуманий приклад є поганим із кількох різних причин. Основний намір прикладу полягав у тому, щоб продемонструвати, що клас розроблений для розширення шляхом надання методів, які при перекритті змінюватимуть поведінку публічних методів без необхідності зміни внутрішнього чи приватного коду. Все-таки я точно не зрозумів OCP.

Відповіді:


10

Якщо ви змінюєте базовий клас, то він насправді не закритий, чи не так!

Подумайте про ситуацію, коли ви випустили бібліотеку у світ. Якщо ви переходите і змінюєте поведінку вашого базового класу, змінивши коефіцієнт понаднормової роботи на 1,5, ви порушили всіх людей, які використовують ваш код, припускаючи, що клас закритий.

Дійсно, щоб зробити клас закритим, але відкритим, вам слід отримати коефіцієнт понаднормової роботи з альтернативного джерела (можливо, конфігураційний файл) або довести віртуальний метод, який можна перекрити?

Якщо клас був справді закритим, то після вашої зміни жоден тестовий випадок не вийшов би з ладу (якщо припустити, що ви маєте 100% охоплення всіма своїми тестовими випадками), і я вважаю, що є тестовий випадок, який перевіряє GetOvertimeFactor() == 2.0M.

Не над інженером

Але не приймайте цей принцип закритого закриття до логічного завершення і майте все, що можна налаштувати з самого початку (тобто над технікою). Визначте лише потрібні вам біти.

Закритий принцип не заважає вам перепроектувати об'єкт. Це просто заважає вам змінити визначений на даний момент публічний інтерфейс на ваш об’єкт ( захищені учасники є частиною загальнодоступного інтерфейсу). Ви можете додати більше функціональних можливостей до тих пір, поки стара функціональність не буде порушена.


"Закритий принцип не заважає вам перепроектувати об'єкт." Власне, це і є . Якщо ви прочитаєте книгу, де вперше був запропонований принцип відкритого закриття, або статтю, яка ввела абревіатуру "OCP", ви побачите, що "нікому не дозволяється вносити зміни до вихідного коду" (крім помилки виправлення).
Rogério

@ Rogério: Це може бути правдою (ще в 1988 році). Але теперішнє визначення (яке стало популярним у 1990 році, коли ОО стало популярним) - все стосується підтримки послідовного публічного інтерфейсу. During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other. en.wikipedia.org/wiki/Open/closed_principle
Martin York

Дякуємо за посилання на Вікіпедію. Але я не впевнений, що "поточне" визначення насправді відрізняється, оскільки воно все ще покладається на спадкування типу (класу чи інтерфейсу). І що цитата "жодних змін у вихідному коді", яку я згадав, походить із статті Роберта Мартіна OCP 1996, яка (нібито) відповідає "поточному визначенню". Особисто я думаю, що Принцип відкритого закриття вже був би забутий, якби Мартін не назвав це абревіатурою, яка, мабуть, має багато маркетингового значення. Сам принцип застарілий і шкідливий, ІМО.
Rogério

3

Отже, відкритий закритий принцип - це загальна думка ... особливо якщо ви намагаєтесь застосувати його одночасно загальна думка YAGNI . Як я дотримуюся обох одночасно? Застосуйте правило трьох . Перший раз, коли ви внесете зміни, зробіть це безпосередньо. І вдруге теж. Третій раз, саме час абстрагуватися, що змінити.

Ще один підхід - "обдури мене один раз ...", коли вам доведеться внести зміни, застосуйте OCP для захисту від цієї зміни в майбутньому . Я майже пішов би так далеко, щоб припустити, що зміна ставки понаднормової роботи - це нова історія. "Як адміністратор заробітної плати я хочу змінити норму понаднормових робіт, щоб я міг відповідати чинному законодавству про працю". Тепер у вас є новий інтерфейс для зміни тарифу понаднормового режиму, спосіб його зберігання, а GetOvertimeFactor () просто запитує у його сховища, що таке норма понаднормових робочих годин.


2

У прикладі, який ви розмістили, коефіцієнт понаднормової роботи повинен бути змінним або постійним. * (Приклад Java)

class PaycheckCalculator {
   float overtimeFactor;

   protected float setOvertimeFactor(float overtimeFactor) {
      this.overtimeFactor = overtimeFactor;
   }

   protected float getOvertimeFactor() {
      return overtimeFactor;
   }
}

АБО

class PaycheckCalculator {
   public static final float OVERTIME_FACTOR = 1.5f;
}

Тоді, коли ви розширюєте клас, встановлюєте чи переосмислюєте фактор. "Чарівні числа" повинні з’являтися лише один раз. Це набагато більше в стилі OCP і DRY (не повторюй себе), тому що не потрібно робити цілком новий клас для іншого фактора, якщо використовувати перший метод, і лише потрібно змінювати константу на один ідіоматичний місце в другому.

Я б застосував перший у випадках, коли було б кілька типів калькулятора, для кожного потрібні різні постійні значення. Прикладом може слугувати схема ланцюжка відповідальності, яка зазвичай реалізується за допомогою успадкованих типів. Об'єкт, який може бачити лише інтерфейс (тобтоgetOvertimeFactor() ), використовує його для отримання всієї необхідної інформації, тоді як підтипи турбуються про фактичну інформацію, яку потрібно надати.

Другий корисний у випадках, коли константа, швидше за все, не зміниться, але використовується в декількох місцях. Мати одну постійну зміну (у тому, що це малоймовірно) - набагато простіше, ніж встановити її всюди або отримати її з файлу властивостей.

Принцип відкритого закриття - це менш заклик не змінювати існуючий об'єкт, ніж застереження залишати інтерфейс для них незмінним. Якщо вам потрібна дещо інша поведінка від класу або додана функціональність для конкретного випадку, розширіть і переосминіть Але якщо вимоги до самого класу змінюються (як-от зміна коефіцієнта), вам потрібно змінити клас. Немає сенсу у величезній ієрархії класів, більшість з яких ніколи не використовується.


Це зміна даних, а не зміна коду. Норма понаднормових робіт не повинна бути сильно кодованою.
Jim C

Здається, у вас є "Get" і "Set Set" назад.
Мейсон Уілер

Ого! повинні були перевірити ...
Майкл К

2

Я не дуже сприймаю ваш приклад як чудове представлення OCP. Я думаю, що це правило насправді означає:

Коли ви хочете додати функцію, вам слід додати лише один клас, і вам не потрібно змінювати будь-який інший клас (але, можливо, конфігураційний файл).

Погана реалізація нижче. Кожен раз, коли ви додаєте гру, вам потрібно буде змінювати клас GamePlayer.

class GamePlayer
{
   public void PlayGame(string game)
   {
      switch(game)
      {
          case "Poker":
              PlayPoker();
              break;

          case "Gin": 
              PlayGin();
              break;

          ...
      }
   }

   ...
}

Клас GamePlayer ніколи не потрібно змінювати

class GamePlayer
{
    ...

    public void PlayGame(string game)
    {
        Game g = GameFactory.GetByName(game); 
        g.Play();   
    }

    ...
}

Тепер припускаючи, що мій GameFactory також дотримується OCP, коли я хочу додати ще одну гру, мені просто потрібно створити новий клас, який успадковується від Game класу, і все повинно просто працювати.

Занадто часто такі класи, як перший, збираються після багатьох років "розширення" і просто ніколи не відновлюються правильно з оригінальної версії (або ще гірше, те, що має бути декілька класів, залишається одним великим класом).

Приклад, який ви надаєте, є OCP-ish. На мій погляд, правильним способом внесення змін до тарифів понаднормового користування був би база даних із збереженими історичними тарифами, щоб дані могли бути перероблені. Код все ж слід закрити для модифікації, оскільки він завжди завантажував відповідне значення з пошуку.

Як приклад із реального світу, я використав варіант свого прикладу, і Принцип відкритого закриття справді світить. Функціональність додати дуже просто, тому що мені просто потрібно вийти з абстрактного базового класу, і моя «фабрика» підбирає її автоматично, а «гравцеві» не важливо, яку конкретну реалізацію повертає фабрика.


1

У цьому конкретному прикладі ви маєте те, що відомо як "Магічне значення". По суті це жорстко закодоване значення, яке може або не може змінюватися з часом. Я спробую вирішити загадку, яку ви висловлюєте загалом, але це приклад типу речі, де створення підкласу - це більше роботи, ніж зміна значення в класі.

Більш ніж ймовірно, ви занадто рано вказали на поведінку в ієрархії свого класу.

Скажімо, у нас є PaycheckCalculator. TheOvertimeFactorБ більш ймовірно , буде ключем від інформації про співробітника. Погодинний працівник може насолоджуватися бонусом за понаднормовий час, тоді як працівник, який отримує заробітну плату, нічого не отримає. Тим не менш, деякі працівники, які отримують заробітну плату, отримають прямий час через контракт, над яким вони працювали. Ви можете вирішити, що існують певні відомі класи сценаріїв оплати, і саме так ви б розвивали свою логіку.

У базовому PaycheckCalculatorкласі ви зробите його абстрактним та вкажете очікувані методи. Основні розрахунки однакові, просто певні фактори обчислюються по-різному. Ви HourlyPaycheckCalculatorб потім реалізувати getOvertimeFactorметод і повернути 1.5 або 2.0 в вашому випадку може бути. Ви StraightTimePaycheckCalculatorреалізуєте getOvertimeFactorдля повернення 1.0. Нарешті, третьою реалізацією буде NoOvertimePaycheckCalculatorте, що реалізує функцію getOvertimeFactorreturn 0.

Ключовим є лише опис поведінки в базовому класі, який планується розширити. Деталі частин загального алгоритму або конкретні значення заповнюватимуться підкласами. Той факт, що ви включили значення за замовчуванням для getOvertimeFactorведе до швидкого і простого "виправлення" до одного рядка, а не для розширення класу, як ви задумали. Він також підкреслює той факт, що для розширення занять докладаються зусилля. Також докладено зусиль для розуміння ієрархії класів у вашій програмі. Ви хочете розробити свої класи таким чином, щоб мінімізувати необхідність створення підкласів, але при цьому забезпечити необхідну гнучкість.

Їжа для роздумів: Коли наші класи містять певні фактори даних, наприклад, OvertimeFactorу вашому прикладі, вам може знадобитися спосіб витягнути цю інформацію з якогось іншого джерела. Наприклад, файл властивостей (оскільки це виглядає як Java) або база даних міститиме значення, а ви PaycheckCalculatorвикористовуєте об'єкт доступу до даних, щоб витягувати ваші значення. Це дозволяє правильним людям змінювати поведінку системи, не вимагаючи перезапису коду.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.