Strategy Pattern in modern C++

In this post, I’m going to talk about the strategy pattern. As I have mentioned in a previous post, my initial inspiration started as I watched ArjanCodes youtube video tutorials. In of those (link), he began to explain the strategy pattern. This approach comes in handy when we build a structure, and we could have several options for implementing a specific procedure. The example that Arjan used is a fictional customer support ticketing system. What does this mean? We have a system where customers report problems. We create a list of the support requests, and we need to server through our support team. How do we serve them? Well, we could follow a FIFO (First In, First Out) approach, or a FILO (First In, Last Out) approach or a random one (although, it doesn’t make sense).  At this point, we have different options to serve the request, and we like to build a flexible structure to achieve that. A structure that is easy to change and adapt to new options without the need of rebuilding from scratch.
The usual starting point is to show the non-optimised code. We can start with the class that represents the Support Ticket, and it looks like:

#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;
}

and the customer support system looks like this:

#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_()};
};

The ProcessTickets class method is not ideal. In case that we need to add a new option, we need to add a new clause in the if..else control statement and build it from scratch. The other weakness is that it has low cohesion since it does not only processing the tickets but also implementing each specific approach.

The first approach to solve that is to create an abstract base class as an interface that declares as pure virtual the ProcessTickets method. Then we subclass the abstract class to implement each specific approach and we replace the string class member with a reference to the abstract that we defined earlier.

More precisely, the interface will look like this:

#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;
};

and implantation of that will look like that:

#include "ticket_ordering_strategy.h"

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

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

Finally, the customer support system will be similar to:

#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_;
};

And how do we use it? Very simple like that:

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;
}

Then, if we want to go wilder, instead of class methods, we could use functor. That means we overload the operator() and we use directly the object.

In that case, the base class is the following:

#include "support_ticket.h"

#include <vector>

using vectorSupportTicket = std::vector<SupportTicket>;

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

The subclasses are:

#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_);
  }
};

and the customer support system is very similar to the previous one:

#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_;
};

Then, when we reach functors, the next logical step is to lambdas. In that case, the entire thing becomes simpler. The Customer Support System will look like:

#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_;
};

and we use it in a straightforward way like that:

#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;
}

Easy, uh?

Credits: