Используете ли вы преимущества принципа открытого-закрытого?

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.

Не сверх инженер

Но не доводите этот принцип открытого закрытия до логического завершения и имейте все, что можно настроить с самого начала (то есть, над разработкой). Определите только те биты, которые вам нужны в данный момент.

Закрытый принцип не мешает вам переделывать объект. Это просто запрещает вам изменять текущий открытый интерфейс на ваш объект ( защищенные члены являются частью открытого интерфейса). Вы все еще можете добавить больше функциональности, если старая функциональность не нарушена.

Мартин Йорк
источник
«Закрытый принцип не мешает вам переделывать объект». На самом деле, это так . Если вы читаете книгу, в которой впервые был предложен принцип Open-Closed, или статью, в которой вводится аббревиатура «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
Мартин Йорк,
Спасибо за ссылку в Википедии. Но я не уверен, что «текущее» определение действительно отличается, так как оно все еще опирается на наследование типа (класса или интерфейса). И эта цитата «без изменений исходного кода», которую я упоминал, взята из статьи Роберта Мартина об OCP 1996, которая (предположительно) соответствует «текущему определению». Лично я думаю, что принцип Open-Closed был бы уже забыт, если бы Мартин не дал ему аббревиатуру, которая, очевидно, имеет большую маркетинговую ценность. Сам принцип устарел и вреден, ИМО.
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()), использует его для получения всей необходимой информации, в то время как подтипы беспокоятся о фактической информации, которую нужно предоставить.

Второе полезно в тех случаях, когда константа вряд ли будет изменена, но используется в нескольких местах. Наличие одной константы для изменения (в маловероятном случае, когда это происходит) намного проще, чем ее установка повсеместно или получение из файла свойств.

Принцип Open-closed - это скорее призыв не изменять существующий объект, а предостережение, чтобы оставить интерфейс для них неизменным. Если вам нужно немного отличное поведение от класса или добавленная функциональность для конкретного случая, расширьте и переопределите. Но если требования к самому классу меняются (например, меняется коэффициент), вам нужно изменить класс. Нет смысла в огромной иерархии классов, большая часть которой никогда не используется.

Майкл К
источник
Это изменение данных, а не изменение кода. Ставка сверхурочной работы не должна была быть жестко закодирована.
Джим С
Похоже, у тебя есть Get и твой 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-иш. По моему мнению, правильный способ обработки изменений ставок сверхурочной работы будет в базе данных с сохранением исторических ставок, чтобы можно было повторно обрабатывать данные. Код все еще должен быть закрыт для модификации, потому что он всегда будет загружать соответствующее значение из поиска.

В качестве примера из реального мира я использовал вариант моего примера, и принцип Open-Closed действительно сияет. Функциональность действительно легко добавить, потому что мне просто нужно наследовать абстрактный базовый класс, и моя «фабрика» выбирает его автоматически, и «плееру» все равно, какую конкретную реализацию возвращает фабрика.

Остин Салонен
источник
1

В этом конкретном примере у вас есть то, что известно как «Волшебная ценность». По существу, жестко закодированное значение, которое может изменяться или не изменяться со временем. Я попытаюсь решить загадку, которую вы выражаете в общих чертах, но это пример того, что создание подкласса - это больше работы, чем изменение значения в классе.

Скорее всего, вы указали поведение слишком рано в вашей иерархии классов.

Допустим, у нас есть PaycheckCalculator. OvertimeFactorБы более вероятно , будет ключом от информации о сотруднике. Почасовой служащий может получать сверхурочные бонусы, а наемный работник не получит ничего. Тем не менее, некоторые наемные работники получат прямое время из-за контракта, над которым они работали. Вы можете решить, что существуют определенные классы сценариев оплаты, и именно так вы бы построили свою логику.

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

Ключ должен описывать только поведение в базовом классе, который предназначен для расширения. Детали частей общего алгоритма или конкретных значений будут заполнены подклассами. Тот факт, что вы добавили значение по умолчанию для getOvertimeFactorотведений, приводит к быстрому и простому «исправлению» одной строки вместо расширения класса, как вы предполагали. Это также подчеркивает тот факт, что есть усилия, связанные с расширением классов. Также приложены усилия для понимания иерархии классов в вашем приложении. Вы хотите спроектировать свои классы таким образом, чтобы свести к минимуму необходимость создания подклассов и обеспечить необходимую гибкость.

Пища для размышления: когда наши классы инкапсулируют определенные факторы данных, как OvertimeFactorв вашем примере, вам может понадобиться способ извлечь эту информацию из какого-то другого источника. Например, файл свойств (поскольку он выглядит как Java) или база данных будут содержать значение, а вы будете PaycheckCalculatorиспользовать объект доступа к данным для получения ваших значений. Это позволяет нужным людям изменять поведение системы без необходимости переписывать код.

Берин Лорич
источник