Паттерн «Стратегия» на современном C++

Добавлено 14 февраля 2022 в 17:44

В этом посте я расскажу о паттерне «стратегия». Этот подход удобен, когда мы строим структуру, и у нас может быть несколько вариантов реализации конкретной процедуры. Например, вымышленная система поддержки клиентов. У нас есть система, в которой клиенты сообщают о проблемах. Мы создаем список запросов в службу поддержки, которые потом нам необходимо обработать. Как мы будем это делать? Ну, мы могли бы следовать подходу FIFO (первым пришел, первым ушел), или подходу FILO (первым пришел, последним ушел), или случайному подходу (хотя это не имеет смысла). На данный момент у нас есть разные варианты обработки запроса, и для достижения этого нам хочется создать гибкую структуру. Структуру, которую легко изменить и адаптировать к новым параметрам без необходимости перестраивать с нуля.

Паттерн «Стратегия» в современном C++

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

#include <random>
#include <iostream>
#include <string>
#include <utility>

class SupportTicket 
{
 public:
  SupportTicket(std::string customer, std::string issue) 
        : customer_{std::move(customer)}, issue_{std::move(issue)} 
  {
    std::random_device rd;
    std::mt19937 mt(rd());
    std::uniform_real_distribution<double> dist(1.0, 1000.0);
    id_ = dist(mt);
  }

  friend std::ostream &operator<<(std::ostream &os, const SupportTicket &st);

 private:
  int id_;
  std::string customer_;
  std::string issue_;
};

std::ostream &operator<<(std::ostream &os, const SupportTicket &st) 
{
  os << "[" << st.id_ << '/' << st.customer_ << '/' << st.issue_ << "]";
  return os;
}

А система поддержки клиентов выглядит так:

#include "support_ticket.h"
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <algorithm>

class CustomerSupport 
{
 public:
  explicit CustomerSupport(std::string processing_strategy)
           : processing_strategy_{std::move(processing_strategy)} 
  {
    tickets_ = std::make_unique<std::vector<SupportTicket>>();
  }

  void CreateTickets(const std::string &customer, const std::string &issue) 
  {
    tickets_->push_back(SupportTicket(customer, issue));
  }

  void ProcessTickets() 
  {
    if (tickets_->empty()) 
    {
      std::cout << "There are no tickets to process. Well done!" << std::endl;
    }

    if (processing_strategy_ == "fifo") 
    {
      for (auto &ticket : *tickets_) 
      {
        std::cout << ticket << std::endl;
      }
    } 
    else if (processing_strategy_ == "filo") 
    {
      std::reverse(tickets_->begin(), tickets_->end());
      for (auto &ticket : *tickets_) 
      {
        std::cout << ticket << std::endl;
      }
    } 
    else if (processing_strategy_ == "random") 
    {
      std::shuffle(tickets_->begin(), tickets_->end(), g_);
      for (auto &ticket : *tickets_) 
      {
        std::cout << ticket << std::endl;
      }
    }
  }

 private:
  std::string processing_strategy_;
  std::unique_ptr<std::vector<SupportTicket>> tickets_;
  std::random_device rd_;
  std::mt19937 g_{rd_()};
};

Метод ProcessTickets этого класса не идеален. В случае если нам нужно добавить новую опцию, нам будет необходимо добавить новое условие в последовательность инструкций if..else и создать его с нуля. Другая слабость заключается в том, что этот метод имеет низкую связность, поскольку он не только обрабатывает заявки, но и реализует каждый конкретный подход.

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

Точнее интерфейс будет выглядеть так:

#include "support_ticket.h"
#include <memory>
#include <vector>

using ptrVectorSupportTicket = std::shared_ptr<std::vector<SupportTicket>>;

class TicketOrderingStrategy 
{
 public:
  virtual void CreateOrdering(ptrVectorSupportTicket list) = 0;
};

А его реализация будет выглядеть так:

#include "ticket_ordering_strategy.h"

using ptrVectorSupportTicket = std::shared_ptr<std::vector<SupportTicket>>;

class FIFOOrderingStrategy : public TicketOrderingStrategy 
{
 public:
  void CreateOrdering(ptrVectorSupportTicket list) override 
  {
  }
};

Наконец, система поддержки клиентов будет похожа на:

#include "support_ticket.h"
#include "ticket_ordering_strategy.h"
#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>

using ptrVectorSupportTicket = std::shared_ptr<std::vector<SupportTicket>>;

class CustomerSupportInheritance 
{
public:
  explicit CustomerSupportInheritance(TicketOrderingStrategy& processing_strategy)
        : processing_strategy_{processing_strategy} 
  {
    tickets_ = std::make_shared<std::vector<SupportTicket>>();
  }

  void CreateTickets(const std::string &customer, const std::string &issue) 
  {
    tickets_->push_back(SupportTicket(customer, issue));
  }

  void ProcessTickets() 
  {
    if (tickets_->empty()) 
    {
      std::cout << "There are no tickets to process. Well done!" << std::endl;
    }

    processing_strategy_.CreateOrdering(tickets_);

    for (auto &ticket : *tickets_) 
    {
      std::cout << ticket << std::endl;
    }
  }

private:
  TicketOrderingStrategy& processing_strategy_;
  ptrVectorSupportTicket tickets_;
};

И как мы ее используем? Очень просто:

include "customer_support_inheritance.h"
#include "ordering_strategies_inheritance.h"

int main() 
{
  FIFOOrderingStrategy ordering_strategy;

  CustomerSupportInheritance my_customer_support{ordering_strategy};

  my_customer_support.CreateTickets("John Smith", "My computer makes strange sounds!");
  my_customer_support.CreateTickets("Linus Sebastian", "I can't upload any videos, please help.");
  my_customer_support.CreateTickets("Arjan Egges", "VSCode doesn't automatically solve my bugs.");
  my_customer_support.ProcessTickets();

  return 0;
}

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

В этом случае базовый класс будет следующим:

#include "support_ticket.h"

#include <vector>

using vectorSupportTicket = std::vector<SupportTicket>;

class TicketOrderingStrategyFunctor 
{
 public:
  virtual void operator()(vectorSupportTicket &list) = 0;
};

Подклассы:

#include <algorithm>
#include "ticket_ordering_strategy_functor.h"

using vectorSupportTicket = std::vector<SupportTicket>;

class [[maybe_unused]] FIFOOrderingStrategyFunctor : public TicketOrderingStrategyFunctor 
{
 public:
  void operator()(vectorSupportTicket &list) override 
  {
  }
};

class [[maybe_unused]] LIFOOrderingStrategyFunctor : public TicketOrderingStrategyFunctor 
{
 public:
  void operator()(vectorSupportTicket &list) override 
  {
    std::reverse(list.begin(), list.end());
  }
};

class [[maybe_unused]] RandomOrderingStrategyFunctor : public TicketOrderingStrategyFunctor 
{
 public:
  void operator()(vectorSupportTicket &list) override 
  {
    std::random_device rd_;
    std::mt19937 g_{rd_()};
    std::shuffle(list.begin(), list.end(), g_);
  }
};

А система поддержки клиентов очень похожа на предыдущую:

#include "support_ticket.h"
#include "ticket_ordering_strategy_functor.h"
#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>

using vectorSupportTicket = std::vector<SupportTicket>;

class CustomerSupportFunctor 
{
 public:
  explicit CustomerSupportFunctor(TicketOrderingStrategyFunctor& processing_strategy)
           : processing_strategy_{processing_strategy} 
  {
    tickets_ = std::vector<SupportTicket>();
  }

  void CreateTickets(const std::string &customer, const std::string &issue) 
  {
    tickets_.push_back(SupportTicket(customer, issue));
  }

  void ProcessTickets() 
  {
    if (tickets_.empty()) 
    {
      std::cout << "There are no tickets to process. Well done!" << std::endl;
    }

    processing_strategy_(tickets_);

    for (auto &ticket : tickets_) 
    {
      std::cout << ticket << std::endl;
    }
  }

 private:
  TicketOrderingStrategyFunctor& processing_strategy_;
  vectorSupportTicket tickets_;
};

Затем, когда мы подошли к функторам, следующим логическим шагом будут лямбда-выражения. В этом случае всё становится проще. Система поддержки клиентов будет выглядеть так:

#include "support_ticket.h"
#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>

using vectorSupportTicket = std::vector<SupportTicket>;

class CustomerSupportLambda 
{
 public:
  explicit CustomerSupportLambda(void (*processing_strategy)(vectorSupportTicket&)) 
           : processing_strategy_{processing_strategy} 
  {
    tickets_ = std::vector<SupportTicket>();
  }

  void CreateTickets(const std::string &customer, const std::string &issue) 
  {
    tickets_.push_back(SupportTicket(customer, issue));
  }

  void ProcessTickets() 
  {
    if (tickets_.empty()) 
    {
      std::cout << "There are no tickets to process. Well done!" << std::endl;
    }

    processing_strategy_(tickets_);

    for (auto &ticket : tickets_) 
    {
      std::cout << ticket << std::endl;
    }
  }

 private:
  void (*processing_strategy_)(vectorSupportTicket&);
  vectorSupportTicket tickets_;
};

И мы используем ее следующим простым способом:

#include "customer_support_lambda.h"

using vectorSupportTicket = std::vector<SupportTicket>;

int main() 
{

  //  CustomerSupportLambda fifo_customer_support{[](vectorSupportTicket list) {}};
  //  CustomerSupportLambda lifo_customer_support{[](vectorSupportTicket list) { std::reverse(list.begin(), list.end()); }};
  CustomerSupportLambda random_customer_support{[](vectorSupportTicket &list) {
    std::random_device rd_;
    std::mt19937 g_{rd_()};
    std::shuffle(list.begin(), list.end(), g_); 
  }};

  random_customer_support.CreateTickets("John Smith", "My computer makes strange sounds!");
  random_customer_support.CreateTickets("Linus Sebastian", "I can't upload any videos, please help.");
  random_customer_support.CreateTickets("Arjan Egges", "VSCode doesn't automatically solve my bugs.");
  random_customer_support.ProcessTickets();

  return 0;
}

Всё просто!

Теги

C++ / CppПаттерн Стратегия (Strategy)Паттерны проектирования / Design PatternsПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.