Skip to content

ООП 14. Паттерны поведения: стратегия (Strategy), команда (Command), цепочка обязанностей (Chain of Responsibility), подписчик издатель (Publish Subscribe), посредник (Mediator). Их преимущества и недостатки.

Kozlitin Maxim edited this page Jun 14, 2023 · 2 revisions

Преимущества использования паттернов:

  • Мы имеем готовое решение
  • За счет готового решения - нюансы все выявлены => надежный код
  • Повышается скорость разработки
  • Повышается читаемость кода
  • Улучшается взаимодействие с коллегами (достаточно сказать название паттерна, который вы используете, и всё сразу станет понятно)

Стратегия

Идея

Нам во время выполнения надо менять реализацию какого-либо метода. Мы можем делать производные классы с разными реализациями и осуществлять "миграцию" между классами во врем выполнения - это неудобно, ибо мы начинаем работать с конкретными типами (классами)

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

Паттерн Стратегия определяет семейство всевозможных алгоритмов.

Плюсы**:**

  • Возможность подмены алгоритма
  • Отделение сущности от реализации
  • Ограничение разрастания иерархии
  • Разъеб(уменьшение) дублирования кода
  • Позволяет добавлять функционал классу

Минусы:

  • Не всегда можем свести к базовой, реализация может быть разная, а мы хотим подменять, алгоритм базируется на обработке данных, а данные могут быть разные
  • Диаграмма(Лек 14_1 14мин):

image

Клиент может установить для нашего класса конкретную стратегию (алгоритм) и, работая с объектом, он будет вызывать этот конкретный алгоритм. Во время работы мы можем этот алгоритм поменять.

Стратегия это вырожденный паттерн МОСТ, только выносим не всю реализацию, а лишь часть - метод

ПИШЕМ ЛЮБОЙ

  • Пример кода 1 Держим указатель. Стратегия (Strategy).

    Объект держит указатель на стратегию. Мы один раз установили стратегию, можем, конечно, её поменять. Вызывая для объекта, вызывается та стратегия, которая нас интересует.

    • Код

      # include <iostream>
      # include <memory>
      # include <vector>
      
      using namespace std;
      
      class Strategy
      {
      public:
          virtual ~Strategy() = 0;
      
          virtual void algorithm() = 0;
      };
      
      Strategy::~Strategy() = default;
      
      class ConStrategy1 : public Strategy
      {
      public:
          virtual void algorithm() override { cout << "Algorithm 1;" << endl; }
      };
      
      class ConStrategy2 : public Strategy
      {
      public:
          virtual void algorithm() override { cout << "Algorithm 2;" << endl; }
      };
      
      class Context
      {
      private:
          unique_ptr<Strategy> strategy;
      
      public:
          explicit Context(Strategy* ptr) : strategy(ptr) {}
      
          void algorithmStrategy() { strategy->algorithm(); }
      };
      
      int main()
      {
          Context obj(new ConStrategy1());
      
          obj.algorithmStrategy();
      }
  • Пример кода 2 Передаем стратегию. Стратегия (Strategy).

    При выполнении мы передаем, какую стратегию хотим использовать. Мы не держим указатель, а устанавливаем при работе.

    • Код

      # include <iostream>
      # include <memory>
      # include <vector>
      
      using namespace std;
      
      class Strategy
      {
      public:
          virtual ~Strategy() = 0;
      
          virtual void algorithm() = 0;
      };
      
      Strategy::~Strategy() = default;
      
      class ConStrategy1 : public Strategy
      {
      public:
          virtual void algorithm() override { cout << "Algorithm 1;" << endl; }
      };
      
      class ConStrategy2 : public Strategy
      {
      public:
          virtual void algorithm() override { cout << "Algorithm 2;" << endl; }
      };
      
      class Context
      {
      public:
      
          void algorithmStrategy(shared_ptr<Strategy> strategy) { strategy->algorithm(); }
      };
      
      int main()
      {
          Context obj;
      
          obj.algorithmStrategy(shared_ptr<Strategy>(new ConStrategy1()));
      
          obj.algorithmStrategy(shared_ptr<Strategy>(new ConStrategy2()));    
      }
  • Пример кода 3 Статический полиморфизм. Стратегия (Strategy).

    Вариант со статическим полиморфизмом. Статический полиморфизм - на этапе компиляции происходит связывание, не можем выбрать на этапе выполнения.

    Единственный плюс этого варианта - быстрее.

    • Код

      # include <iostream>
      # include <memory>
      # include <vector>
      
      using namespace std;
      
      class Strategy
      {
      public:
          virtual ~Strategy() = 0;
      
          virtual void algorithm() = 0;
      };
      
      Strategy::~Strategy() = default;
      
      class ConStrategy1 : public Strategy
      {
      public:
          virtual void algorithm() override { cout << "Algorithm 1;" << endl; }
      };
      
      class ConStrategy2 : public Strategy
      {
      public:
          virtual void algorithm() override { cout << "Algorithm 2;" << endl; }
      };
      
      template <typename CStrategy>
      class Context
      {
      private:
          unique_ptr<CStrategy> strategy;
      
      public:
          Context() : strategy(new CStrategy()) {}
      
          void algorithmStrategy() { strategy->algorithm(); }
      };
      
      int main()
      {
          Context<ConStrategy1> obj;
      
          obj.algorithmStrategy();
      }

Команда

Идея

Возможны разные запросы (загрузить, повернуть, перенести и подобное). Идея такая: обернуть каждый запрос в отдельный класс (класс может быть как простой, так и составной). Если мы чётко знаем, кто должен обработать запрос, то надо держать объект (обработчик) и указатель на метод, и их можно передавать тому, кто это обработает.

Диаграмма

Запрос, или команда, идет в виде объекта. Базовый класс - и у нас может быть не одна команда, а несколько, в зависимости от того, что нам нужно. Команда может нести данные, может нести что надо сделать, (указатель на метод какого-либо объекта, например.)

  • UML

image-2

  • Пример кода. Команда (Command).

    # include <iostream>
    # include <memory>
    
    using namespace std;
    
    class Command
    {
    public:
        virtual ~Command() = default;
        virtual void execute() = 0;
    };
    
    template <typename Reseiver>
    class SimpleCommand : public Command
    {
    private:
        typedef void(Reseiver::*Action)();
    
        shared_ptr<Reseiver> reseiver;
        Action action;
    
    public:
        SimpleCommand(shared_ptr<Reseiver> r, Action a) : reseiver(r), action(a) {}
    
        virtual void execute() override { ((*reseiver).*action)(); }
    };
    
    class Object
    {
    public:
        void run() { cout << "Run method;" << endl; }
    };
    
    int main()
    {
        shared_ptr<Object> obj(new Object());
        shared_ptr<Command> command(new SimpleCommand<Object>(obj, &Object::run));
    
        command->execute();
    }

Преимущества

  1. Унификация обработки запросов к системе (событий)
  2. Уменьшается зависимость между объектами, не нужно держать связь
  3. Команду можем выполнить не сразу, а через время, можно сформировать очередь
  4. Если добавить к команде композит, то можно формировать сложные команды из нескольких команд
    • UML

Цепочка обязанностей

Часто задача сводится к тому, что запрос должны обрабатывать несколько объектов, а не один.

  • Идея: создать цепочку обработчиков.
  • Преимущества: позволяет передавать запрос последовательно по цепочке обработчиков, каждый обработчик сам решает, передавать дальше или нет.
  • Диаграмма

image-3

Handler определяет общий интерфейс и задает механизм передачи запроса, а каждый Handler1..n содержат код обработки запросов

  • Пример кода. Цепочка обязанностей (Chain of Responsibility).

    Рекурсивный список

    В примерах – идем по цепочке, пока не наткнемся на обработчик с состоянием true, дальше по цепочке не пойдем.

    # include <iostream>
    # include <memory>
    
    using namespace std;
    
    class AbstractHandler
    {
    protected:
        shared_ptr<AbstractHandler> next;
    
        virtual bool run() = 0;
    
    public:
        using Default = shared_ptr<AbstractHandler>;
    
        virtual ~AbstractHandler() = default;
    
        virtual bool handle() = 0;
    
        void add(shared_ptr<AbstractHandler> node);
        void add(initializer_list<shared_ptr<AbstractHandler>> list);
    };
    
    class ConHandler : public AbstractHandler
    {
    private:
        bool condition{ false };
    
    protected:
        virtual bool run() override { cout << "Method run;" << endl; return true; }
    
    public:
        ConHandler() = default;
        ConHandler(bool c) : condition(c) { cout << "Constructor;" << endl; }
        virtual ~ConHandler() override { cout << "Destructor;" << endl; }
    
        virtual bool handle() override
        {
            if (!condition) return next ? next->handle() : false;
    
            return run();
        }
    
    };
    
    void AbstractHandler::add(shared_ptr<AbstractHandler> node)
    {
        if (next)
            next->add(node);
        else
            next = node;
    }
    
    void AbstractHandler::add(initializer_list<shared_ptr<AbstractHandler>> list)
    {
    		for (auto elem : list)
    			add(elem);
    }
    
    int main()
    {
        shared_ptr<AbstractHandler> chain(new ConHandler());
    
        chain->add({
            shared_ptr<AbstractHandler>(new ConHandler(false)),
            shared_ptr<AbstractHandler>(new ConHandler(true)),
            shared_ptr<AbstractHandler>(new ConHandler(true)),
            AbstractHandler::Default()}
        );
    
        cout << "Result = " << chain->handle() << ";" << endl;;
    }
  • Когда надо использовать?

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

Подписчик-издатель

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

Этот паттерн задает механизм: тот, кто хочет принять сообщение, тот подписывается у издателя (подписался - получил, не подписался - не получил)

Группа объектов реагирует на один объект, на его изменения, издатель оповещает всех подписчиков, когда происходят изменения, вызывая их методы

  • Диаграмма (у Publisher еще один метод - unsubscribe())

image-5

  • Пример кода. Синхронно

    #include <iostream>
    #include <memory>
    #include <vector>
    
    using namespace std;
    
    class Subscriber;
    
    using Reseiver = Subscriber;
    
    class Publisher {
        using Action = void (Reseiver::*)();
        using Pair = pair<shared_ptr<Reseiver>, Action>;
    
     private:
        vector<Pair> callback; // храним пару подписчик-метод
    
        int indexOf(shared_ptr<Reseiver> r);
    
     public:
        bool subscribe(shared_ptr<Reseiver> r, Action a);
        bool unsubscribe(shared_ptr<Reseiver> r);
        void run();
    };
    
    class Subscriber {
     public:
        virtual ~Subscriber() = default;
    
        virtual void method() = 0;
    };
    
    class ConSubscriber : public Subscriber {
     public:
        virtual void method() override { cout << "method;" << endl; }
    };
    
    #pragma region Methods Publisher
    bool Publisher::subscribe(shared_ptr<Reseiver> r, Action a) {
        if (indexOf(r) != -1) return false;
    
        Pair pr(r, a);
    
        callback.push_back(pr);
    
        return true;
    }
    
    bool Publisher::unsubscribe(shared_ptr<Reseiver> r) {
        int pos = indexOf(r);
    
        if (pos != -1) callback.erase(callback.begin() + pos);
    
        return pos != -1;
    }
    
    void Publisher::run() {
        cout << "Run:" << endl;
        for (auto elem : callback) ((*elem.first).*(elem.second))();
    }
    
    int Publisher::indexOf(shared_ptr<Reseiver> r) {
        int i = 0;
        for (auto it = callback.begin(); it != callback.end() && r != (*it).first;
             i++, ++it)
            ;
    
        return i < callback.size() ? i : -1;
    }
    #pragma endregion
    
    int main() {
        shared_ptr<Subscriber> subscriber1 = make_shared<ConSubscriber>();
        shared_ptr<Subscriber> subscriber2 = make_shared<ConSubscriber>();
        shared_ptr<Publisher> publisher = make_shared<Publisher>();
    
        publisher->subscribe(subscriber1, &Subscriber::method);
        if (publisher->subscribe(subscriber2, &Subscriber::method))
            publisher->unsubscribe(subscriber1);
    
        publisher->run();
    }
  • Пример кода. Асинхронно.

    # include <iostream>
    # include <string>
    # include <QObject>
    
    using namespace std;
    
    class Employer : public QObject // Publisher
    {
        Q_OBJECT
    
    private:
        const int day_salary = 15;
        const int day_advance = 25;
    
    public:
        Employer() = default;
        void notifyEmployeis(int day);
    
    signals:
        void salaryPayment();
        void taskIssuance();
    };
    
    # pragma region Method Publisher
    void Employer::notifyEmployeis(int day)
    {
        if (day == day_salary || day == day_advance)
        {
            emit salaryPayment();
        }
        else
        {
            emit taskIssuance();
        }
    }
    
    # pragma endregion
    
    enum class Mood
    {
        happy,
        sad
    };
    
    class Employee : public QObject // Subscribe
    {
        Q_OBJECT
    
    private:
        string name;
        Mood mood;
    
    public:
        Employee(string name) : name(name), mood(Mood::sad) {}
    
        string getName() { return name; }
    	Mood getMood() { return mood; }
    
        void subscribeOnEmployer(const Employer* ptrEmployer);
        void unsubscribeFromEmployer(const Employer* ptrEmployer);
    
    public slots:
        void onSalaryPayment() { mood = Mood::happy; }
        void onTaskIssuance() { mood = Mood::sad; }
    };
    
    # pragma region Methods Subscribe
    void Employee::subscribeOnEmployer(const Employer* ptrEmployer)
    {
        QObject::connect(ptrEmployer, SIGNAL(salaryPayment()), this, SLOT(onSalaryPayment()));
        QObject::connect(ptrEmployer, SIGNAL(taskIssuance()), this, SLOT(onTaskIssuance()));
    }
    
    void Employee::unsubscribeFromEmployer(const Employer* ptrEmployer)
    {
        QObject::disconnect(ptrEmployer, SIGNAL(salaryPayment()), this, SLOT(onSalaryPayment()));
        QObject::disconnect(ptrEmployer, SIGNAL(taskIssuance()), this, SLOT(onTaskIssuance()));
    }
    
    # pragma endregion
    
    ostream& operator<< (ostream &out, const Employee &employee)
    {
        string mood = employee.getMood() == Mood::happy ?
                    "Happy with a lot of money!!!!))))" :
                    "Sad with a lot of work....((((";
        
        return out << "Name: " << employee.getName() << ", Mood: " << mood << endl;
    }
    
    int main()
    {
        //Создадим работодателя, который выступает в роли объекта publisher
        Employer google;
    
        //Создадим несколько работников, которые выступают в роли объекта subscriber
        Employee vanya("Vanya");
        Employee artem("Artem");
        Employee maxim("Maxim");
        Employee dmitrii("Dmitrii");
    
        //Проверим текущее состояние работников
        cout << "Employees states after creation:" << endl;
        cout << vanya;
        cout << artem;
        cout << maxim;
        cout << dmitrii << endl;
    
        //Подпишем нескольких работников на объект работодателя
        vanya.subscribeOnEmployer(&google);
        artem.subscribeOnEmployer(&google);
        dmitrii.subscribeOnEmployer(&google);
    
        //Проверим состояние после подписки на объект работодателя
        cout << "Employees states after publishing:" << endl;
        cout << vanya;
        cout << artem;
        cout << maxim;
        cout << dmitrii << endl;
    
        //Вызовем метод получения оповещения для всех подписчиков, в котором вызовется сигнал salaryPayment
        int day_salary = 25;
        google.notifyEmployeis(day_salary);
    
        //Проверим состояние работников, после получения зарплаты
        cout << "Employees states after salary event:" << endl;
        cout << vanya;
        cout << artem;
        cout << maxim;
        cout << dmitrii << endl;
    
        //Вызовем метод получения оповещения для всех подписчиков, в котором вызовется сигнал taskIssuance
        int someDay = 10;
        google.notifyEmployeis(someDay);
    
        //Проверим состояние работников, после получения задания
        cout << "Employees states after task event:" << endl;
        cout << vanya;
        cout << artem;
        cout << maxim;
        cout << dmitrii << endl;
    
        return 0;
    }

Издатель держит список объектов, которые на него подписались. Вектор пар: подписчик и метод подписчика, который необходимо вызвать. У издателя должны быть методы подписаться() и отписаться().

Отправленный сигнал может обрабатываться подписчиками синхронно и асинхронно.

  • Преимущества:
    • Издатели не зависят от подписчиков
    • Схема гибкая: можно как подписаться, так и отписаться (надо делать iterator)
  • Недостатки:
    • издатель должен держать список объектов, которые на него подписаны (паттерн тяжёлый)
    • нет порядка в оповещении подписчиков
    • если издателей много, (и каждый подписчик может быть издателем), связей будет очень много

Посредник

  • Идея: (последний недостаток подписчика-издателя*(издателей много - много связей)*) связи вынести в отдельный объект, тогда каждый объект будет обращаться к этому отдельному объекту, а он в свою очередь будет искать нужные связи (с кем ему связаться).

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

  • Диаграмма

image-4

  • Пример кода. Посредник (Mediator)

    В данном примере кода будут от левого, отправляться всем правым, а от правого, всем левым

    # include <iostream>
    # include <memory>
    # include <list>
    # include <vector>
    
    using namespace std;
    
    class Message {};         // Request
    
    class Mediator;
    
    class Colleague
    {
    private:
    	weak_ptr<Mediator> mediator;
    
    public:
    	virtual ~Colleague() = default;
    
    	void setMediator(shared_ptr<Mediator> mdr) { mediator = mdr; }
    
    	virtual bool send(shared_ptr<Message> msg);
    	virtual void receive(shared_ptr<Message> msg) = 0;
    };
    
    class ColleagueLeft : public Colleague
    {
    public:
    	virtual void receive(shared_ptr<Message> msg) override { cout << "Right - > Left;" << endl; }
    };
    
    class ColleagueRight : public Colleague
    {
    public:
    	virtual void receive(shared_ptr<Message> msg) override { cout << "Left - > Right;" << endl; }
    };
    
    class Mediator
    {
    protected:
    	list<shared_ptr<Colleague>> colleagues;
    
    public:
    	virtual ~Mediator() = default;
    
    	virtual bool send(const Colleague* coleague, shared_ptr<Message> msg) = 0;
    
    	static bool add(shared_ptr<Mediator> mediator, initializer_list<shared_ptr<Colleague>> list);
    };
    
    class ConMediator : public Mediator
    {
    public:
    	virtual bool send(const Colleague* coleague, shared_ptr<Message> msg) override;
    };
    
    #pragma region Methods Colleague
    bool Colleague::send(shared_ptr<Message> msg)
    {
    	shared_ptr<Mediator> mdr = mediator.lock();
    
    	return mdr ? mdr->send(this, msg) : false;
    }
    #pragma endregion
    
    #pragma region Methods Mediator
    bool Mediator::add(shared_ptr<Mediator> mediator, initializer_list<shared_ptr<Colleague>> list)
    {
    	if (!mediator || list.size() == 0) return false;
    
    	for (auto elem : list)
    	{
    		mediator->colleagues.push_back(elem);
    		elem->setMediator(mediator);
    	}
    
    	return true;
    }
    
    bool ConMediator::send(const Colleague* colleague, shared_ptr<Message> msg)
    {
    	bool flag = false;
    	for (auto& elem : colleagues)
    	{
    		if (dynamic_cast<const ColleagueLeft*>(colleague) && dynamic_cast<ColleagueRight*>(elem.get()))
    		{
    			elem->receive(msg);
    			flag = true;
    		}
    		else if (dynamic_cast<const ColleagueRight*>(colleague) && dynamic_cast<ColleagueLeft*>(elem.get()))
    		{
    			elem->receive(msg);
    			flag = true;
    		}
    	}
    
    	return flag;
    }
    #pragma endregion
    
    int main()
    {
    	shared_ptr<Mediator> mediator =make_shared<ConMediator>();
    
    	shared_ptr<Colleague> col1 = make_shared<ColleagueLeft>();
    	shared_ptr<Colleague> col2 = make_shared<ColleagueRight>();
    	shared_ptr<Colleague> col3 = make_shared<ColleagueLeft>();
    	shared_ptr<Colleague> col4 = make_shared<ColleagueLeft>();
    
    	Mediator::add(mediator, { col1, col2, col3, col4 });
    
    	shared_ptr<Message> msg = make_shared<Message>();
    
    	col1->send(msg);
    	col2->send(msg);
    }
    /*
    Left - > Right;
    Right - > Left;
    Right - > Left;
    Right - > Left;
    */

Конкретный медиатор устанавливает, кому передается и сообщение.

  • Преимущества:
    • Упрощаются объекты (выносим из них связи)
    • Нет прямой зависимости между ними
    • Происходит централизованное взаимодействие (появляется контроль)
  • Недостатки:
    • Использование посредников замедляет выполнение программы
Clone this wiki locally