Strategy Pattern in C++
Let’s imagine we are a duck object creator. There are many kinds of duck. Creating an abstract class for the duck species that first come to our mind. Because they have common features. They all have a view. They can all swim, quack and walk. We would probably write our abstract class as follows;
class duck
{
virtual void swim() = 0;
virtual void walk() = 0;
virtual void quack() = 0;
virtual void wiev() = 0;
}
They all can swim but how do they swim? They can all quack, but how do they quack? We can implement how each bird species swims in its own class’s own method. Do you agree? Let’s try to do it briefly;
class MallerdDuck : public Duck
{
void swim() override
{
std::cout << "Mallerd Duck swim\n";
}
void walk() override
{
std::cout << "Mallerd Duck walk\n";
}
void quack() override
{
std::cout << "Mallerd Duck quack\n";
}
void wiev() override
{
std::cout << "Mallerd Duck wiev\n";
}
};
Well, very logical, simple, easy to understand implementation. What if a duck we created can not quack? So it is mute Duck. What do we do then?
The interface we created gives us the obligation to write the quack function. Then does the code below make sense?
class RuddyDuck : public Duck
{
void swim() override
{
std::cout << "Mallerd Duck swim\n";
}
void walk() override
{
std::cout << "Mallerd Duck walk\n";
}
void quack() override
{
}
void wiev() override
{
std::cout << "Mallerd Duck wiev\n";
}
};
Well, we have provided a duck that can not quack.
But If you are asked which ducks cannot quack, they all seem to quacking duck. Does this make sense to you? Never mind, This code works for us. Actually I also used to do this before :)
Everything is well, So what if a duck loses its ability to quack at runtime?
What will we do now? I think the design has become a little difficult. Some developers encountered such situations years ago and a solution was found. It is “Strategy Pattern”.
The Strategy Pattern is a behavioral design pattern in C++ (and other object-oriented programming languages) that allows you to define a family of algorithms, encapsulate each one of them, and make them interchangeable. It lets the algorithm vary independently from clients that use it.
In the context of the Strategy Pattern, you have three main components:
- Context: This is the class that uses a strategy. It has a reference (or pointer) to a strategy object and can switch between different strategies dynamically.
- Strategy: This is an interface (or an abstract base class) that defines a set of methods that concrete strategies must implement. It represents the family of algorithms. Each concrete strategy implements a specific algorithm.
- Concrete Strategies: These are the concrete implementations of the Strategy interface. Each concrete strategy provides its own implementation for the algorithm defined in the interface.
Let’s we write our code according to above information for our quack problem;
#include <iostream>
#include <memory>
class QuackBehaviour
{
public:
virtual ~QuackBehaviour() = default;
virtual void quack() const = 0;
};
class Quack : public QuackBehaviour
{
public:
void quack() const override
{
std::cout << "Quack\n";
}
};
class MuteQuack : public QuackBehaviour
{
public:
void quack() const override
{
std::cout << "<<Silence>>\n";
}
};
class Duck
{
private :
std::unique_ptr<QuackBehaviour> _quackBehaviour;
public:
explicit Duck(std::unique_ptr<QuackBehaviour> &&strategy) : _quackBehaviour(std::move(strategy))
{
}
void setQuackStrategy(std::unique_ptr<QuackBehaviour> &&strategy)
{
_quackBehaviour = std::move(strategy);
}
void performQuack()
{
_quackBehaviour->quack();
}
};
int main()
{
Duck duck(std::make_unique<Quack>());
duck.performQuack();
duck.setQuackStrategy(std::make_unique<MuteQuack>());
duck.performQuack();
}
QuackBehavior is an abstract base class that defines a quacking behavior. It has a pure virtual function quack().
Quack and MuteQuack are concrete quacking behaviors that inherit from QuackBehavior and provide specific implementations of the quack() method.
The Duck class has a private member, _quackBehaviour, which is a unique pointer to a QuackBehavior object. This member represents the quacking behavior of the duck.
The Duck class's constructor takes a unique pointer to a QuackBehavior as an argument, allowing you to set the initial quacking behavior when creating a duck.
The setQuackStrategy() method allows you to change the quacking behavior of the duck at runtime by replacing the current strategy with a new one.
The performQuack() method calls the quack() method of the currently set quacking behavior, allowing the duck to perform its quacking action.
In the main function, we create a Duck object with an initial quacking behavior of Quack, and then we change the quacking behavior to MuteQuack using the setQuackStrategy() method. When we call performQuack(), the duck will exhibit the corresponding behavior based on the currently set strategy. This demonstrates how we can use the Strategy Pattern to switch between different behaviors for a class without modifying its core implementation.
Remember this duck is just an example. You can use this pattern your business life.
Let’s do another example.
We assume we are a software developer in a company working on smart cities. We are developing software for bus cards to be used in city transportation. Balance can be loaded to every card and balance can be refunded to every card. But subscriptions may not be loaded to every card. Subscription installation at run time may be conditional.
Our approach is to wrap things that may change.
Balance top-up and balance refund process is valid for every card, but subscription top-up is not valid for every card. What changes in this example is the subscription loading. So we should wrap subscription loading. let’s do it briefly;
#include <iostream>
#include <memory>
class CardBehavior
{
public:
virtual ~CardBehavior() = default;
virtual void loadBalance(int amount) const = 0;
virtual void refundBalance(int amount) const = 0;
};
class SubscriptionBehavior
{
public:
virtual ~SubscriptionBehavior() = default;
virtual void subscription() const = 0;
};
class SubscriptionValid : public SubscriptionBehavior
{
public:
void subscription() const override
{
std::cout << "Subscription is valid !\n";
}
};
class SubscriptionInValid : public SubscriptionBehavior
{
public:
void subscription() const override
{
std::cout << "Subscription is InValid !\n";
}
};
class Card : public CardBehavior
{
private :
std::unique_ptr<SubscriptionBehavior> _subscriptionBehavior;
public:
explicit Card(std::unique_ptr<SubscriptionBehavior> &&strategy)
: _subscriptionBehavior(std::move(strategy))
{
}
void setSubscriptionStrategy(std::unique_ptr<SubscriptionBehavior> &&strategy)
{
_subscriptionBehavior = std::move(strategy);
}
void performSubscription()
{
_subscriptionBehavior->subscription();
}
void loadBalance(int amount) const override
{
std::cout << "Load Balance : "<<amount <<"!\n";
}
void refundBalance(int amount) const override
{
std::cout << "Refund Balance : "<<amount <<"!\n";
}
};
int main()
{
Card card(std::make_unique<SubscriptionValid>());
card.performSubscription();
bool isAStudent = false;
if (!isAStudent)
{
card.setSubscriptionStrategy(std::make_unique<SubscriptionInValid>());
}
card.performSubscription();
card.loadBalance(200);
card.refundBalance(200);
}