В чем выгода от использования паттерна стратегии?


Я посмотрел на это объяснение в Википедии, в частности в Примере C++, и не смог распознать разницу между простым определением 3 классов, созданием экземпляров и их вызовом и этим примером. То, что я видел, было просто помещением двух других классов в процесс и не может видеть, где будет польза. Теперь я уверен, что упускаю что - то очевидное (дерево для деревьев)-не мог бы кто-нибудь объяснить это на конкретном примере из реального мира?


Что я могу сделать из ответов до сих пор мне кажется, что это просто более сложный способ сделать это:

have an abstract class: MoveAlong with a virtual method: DoIt()
have class Car inherit from MoveAlong, 
     implementing DoIt() { ..start-car-and-drive..}
have class HorseCart inherit from MoveAlong, 
     implementing DoIt() { ..hit-horse..}
have class Bicycle inherit from MoveAlong, 
     implementing DoIt() { ..pedal..}
now I can call any function taking MoveAlong as parm 
passing any of the three classes and call DoIt
Isn't this what Strategy intents? (just simpler?)

[Edit-update] Функция, на которую я ссылаюсь выше, заменяется другим классом, в котором MoveAlong будет атрибутом, который устанавливается в соответствии с необходимостью на основе алгоритма, реализованного в этом новом классе. (Аналогично тому, что показано в принятом ответе.)


[Edit-update] заключение

Паттерн стратегии имеет свое применение, но я твердо верю в KISS, и склонялся бы к более прямолинейным и менее запутанным приемам. В основном потому, что я хочу передать легко поддерживаемый код (и потому, что я, скорее всего, буду тем, кто должен внести изменения!).

8 12

8 ответов:

Суть в том, чтобы разделить алгоритмы на классы, которые могут быть подключены во время выполнения. Например, предположим, что у вас есть приложение, которое включает в себя часы. Есть много различных способов, которыми вы можете нарисовать часы, но по большей части основная функциональность Одна и та же. Таким образом, вы можете создать интерфейс отображения часов:

class IClockDisplay
{
    public:
       virtual void Display( int hour, int minute, int second ) = 0;
};

Затем у вас есть класс Clock, который подключен к таймеру и обновляет отображение часов один раз в секунду. Чтобы у тебя было что-то например:

class Clock
{
   protected:
      IClockDisplay* mDisplay;
      int mHour;
      int mMinute;
      int mSecond;

   public:
      Clock( IClockDisplay* display )
      {
          mDisplay = display;
      }

      void Start(); // initiate the timer

      void OnTimer()
      {
         mDisplay->Display( mHour, mMinute, mSecond );
      }

      void ChangeDisplay( IClockDisplay* display )
      {
          mDisplay = display;
      }
};

Затем во время выполнения вы создаете экземпляр часов с соответствующим классом отображения. то есть вы могли бы иметь ClockDisplayDigital, ClockDisplayAnalog, ClockDisplayMartian, реализующие интерфейс IClockDisplay.

Таким образом, вы можете позже добавить любой тип нового отображения часов, создав новый класс без необходимости возиться с вашим классом часов и без необходимости переопределять методы, которые могут быть грязными для обслуживания и отладки.

В Java вы используете входной поток шифра для расшифровки следующим образом:

String path = ... ;
InputStream = new CipherInputStream(new FileInputStream(path), ???);
Но поток шифров не знает, какой алгоритм шифрования вы собираетесь использовать или размер блока, стратегию заполнения и т. д... Новые алгоритмы будут добавляться все время, поэтому их жесткое кодирование нецелесообразно. Вместо этого мы передаем в шифробъект Стратегии , чтобы рассказать ему, как выполнить дешифровку...
String path = ... ;
Cipher strategy = ... ;
InputStream = new CipherInputStream(new FileInputStream(path), strategy);

В общем случае вы используете шаблон стратегии всякий раз, когда у вас есть какой-либо объект, который знает что ему нужно сделать, но не как это сделать. Другой хороший пример-менеджеры по верстке в Swing, хотя в этом случае это не сработало так же хорошо, см. Totally GridBag для забавной иллюстрации.

NB: здесь работают два паттерна, так как обертывание потоков в потоки является примеромдекоратора .

Существует разница между стратегией и решением/выбором. Большую часть времени a мы будем обрабатывать решения/варианты в нашем коде и реализовывать их с помощью конструкций if()/switch (). Паттерн стратегии полезен, когда есть необходимость отделить логику / алгоритм от использования.

В качестве примера рассмотрим механизм опроса, в котором разные пользователи будут проверять наличие ресурсов/обновлений. Теперь мы можем захотеть, чтобы некоторые из приватизированных пользователей были уведомлены с более быстрым временем оборота или с большим количеством подробности. По существу, используемая логика изменяется в зависимости от ролей пользователей. Стратегия имеет смысл с точки зрения дизайна / архитектуры, на более низких уровнях детализации она всегда должна быть поставлена под сомнение.

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

Стратегический подход дает некоторые преимущества:

  • Вы можете изменить стратегию во время выполнения - сравните это с изменением типа класса во время выполнения, что гораздо сложнее, специфично для компилятора и невозможно для невиртуальных методов
  • Один основной класс может использовать несколько стратегий, что позволяет рекомбинировать их несколькими способами. Рассмотрим класс, который ходит по дереву и вычисляет функцию на основе каждого узла и текущего результата. У вас может быть стратегия ходьбы (глубина-первая или ширина-первая) и стратегия вычисления (некоторый функтор - т. е. "подсчет положительных чисел" или "сумма"). Если вы не используете стратегии, вам нужно будет реализовать подкласс для каждой комбинации ходьбы/расчета.
  • код легче поддерживать, так как изменение или понимание стратегии не требует от вас понимания всего основного объекта
Недостатком является то, что во многих случаях паттерн стратегии является излишним - оператор switch/case существует не просто так. Рассмотрите возможность начать с простых операторов потока управления (switch / case или if), а затем только при необходимости перейти к классу иерархия и если у вас есть более одного измерения изменчивости, извлеките из него стратегии. Указатели функций находятся где-то в середине этого континуума.

Рекомендуемое чтение:

Один из способов взглянуть на это-когда у вас есть множество действий, которые вы хотите выполнить, и эти действия определяются во время выполнения. Если вы создадите хэш-таблицу или словарь стратегий, то сможете извлечь те стратегии, которые соответствуют значениям команд или параметрам. Как только ваше подмножество выбрано, вы можете просто повторить список стратегий и выполнить их последовательно.

Одним из конкретных примеров может быть вычисление суммы заказа. Ваши параметры или команды будут базовыми цена, местный налог, городской налог, государственный налог, наземная доставка и скидка по купону. Гибкость вступает в игру, когда вы обрабатываете вариации заказов - в некоторых штатах не будет налога с продаж, в то время как другие заказы должны будут применять купон. Вы можете динамически назначать порядок вычислений. До тех пор, пока вы учитывали все свои вычисления, вы можете приспособить все комбинации без повторной компиляции.

Этот шаблон проектирования позволяетинкапсулировать алгоритмы в классы.

Класс, использующий стратегию, клиентский класс, отделен от реализации алгоритма. Вы можете изменить реализацию алгоритмов или добавить новый алгоритм, не изменяя клиента. Это также может быть сделано динамически: клиент может выбрать алгоритм, который он будет использовать.

Для примера представьте приложение, которому нужно сохранить изображение в файл ; изображение можно сохранить в различные форматы (PNG, JPG ...). Все алгоритмы кодирования будут реализованы в различных классах, использующих один и тот же интерфейс. Клиентский класс выберет один из них в зависимости от предпочтений пользователя.

В Примере Википедии эти экземпляры могут быть переданы в функцию, которая не должна заботиться о том, к какому классу принадлежат эти экземпляры. Функция просто вызывает execute на переданном объекте и знает, что произойдет правильная вещь.

Типичным примером паттерна стратегии является то, как файлы работают в Unix. Учитывая файловый дескриптор, вы можете читать из него, записывать в него, опрашивать его, искать на нем, отправлять ему ioctls и т. д., без необходимости знать, имеете ли вы дело с файлом, каталогом, труба, гнездо, устройство и т. д. (Конечно, некоторые операции, такие как seek, не работают с трубами и розетками. Но в этих случаях чтение и запись будут работать просто отлично.) Это означает, что вы можете написать общий код для обработки всех этих различных типов "файлов", без необходимости писать отдельный код для работы с файлами и каталогами и т. д. Ядро Unix заботится о делегировании вызовов правильному коду.

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

Паттерн стратегии работает на простой идее, т. е. "предпочтение композиции перед наследованием", так что стратегия/алгоритм могут быть изменены во время выполнения. Чтобы проиллюстрировать это, возьмем пример, в котором нам нужно зашифровать различные сообщения, основанные на его типе, например, MailMessage, ChatMessage и т. д.

class CEncryptor
{
    virtual void encrypt () = 0;
    virtual void decrypt () = 0;
};
class CMessage
{
private:
    shared_ptr<CEncryptor> m_pcEncryptor;
public:
    virtual void send() = 0;

    virtual void receive() = 0;

    void setEncryptor(cost shared_ptr<Encryptor>& arg_pcEncryptor)
    {
        m_pcEncryptor =  arg_pcEncryptor;
    }

    void performEncryption()
    {
        m_pcEncryptor->encrypt();
    }
};

Теперь во время выполнения вы можете создавать экземпляры различных сообщений, унаследованных от CMessage (например, CMailMessage:public CMessage) с различными шифраторами (например, CDESEncryptor:public CEncryptor)

CMessage *ptr = new CMailMessage();
ptr->setEncryptor(new CDESEncrypto());
ptr->performEncryption();