Developers usually spend more time maintaining and adding new features into an existing system instead of building new ones. It is so critical to have a good system design that is able to accommodate changes. In this post let’s go through one of the most adopted design patterns by starting with a problematic design and evolving gradually to make our system extensible and reusable.
A high-level explanation would be
The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently of clients that use it.
It must be confusing with the above abstraction, but don’t worry, in the following, we will walk through an example and identify the problems and make progress step by step to understand the key principles of the strategy pattern.
Let’s say we want to introduce a class of Ducks, which includes redhead duck
, mallard duck
and etc. Each of them would have some commonly shared functionalities like, swim
, display
, and fly
, take a minute and think about how would you address this problem in your system design? (code here)
The easy piece, right? The first thing we can leverage in OO programming is to leverage inheritance, we would have a superclass called Duck
and have other subclasses inherit the functionalities of our parent class. Something like this,
class Duck(ABC):
def __init__(self):
pass
@abstractmethod
def display(self):
pass
@abstractmethod
def swim(self):
pass
@abstractmethod
def quack(self):
pass
class RedheadDuck(Duck):
def swim(self):
return "paddling"
def quack(self):
return "redhead quack"
def display(self):
return "redhead duck"
Looks all good! But now there comes a new requirement, we want to introduce a new functionality to our ducks, which is fly
. What do we do about it? We can have a fly
method in our superclass and all child classes would inherit this method automatically.
class Duck(ABC):
def __init__(self):
pass
@abstractmethod
def display(self):
pass
@abstractmethod
def swim(self):
pass
@abstractmethod
def quack(self):
pass
def fly(self):
return "fly with wings"
That seems no harm, but in the meantime, our products introduced a new kind of duck, rubber duck
, by inheriting the Duck
class, what we would see is,
class RubberDuck(Duck):
def display(self):
return "rubber duck"
def swim(self):
return "floating"
def quack(self):
return "jioooo"
rubber_duck = RubberDuck()
print(rubber_duck.fly())
# "fly with wings"
Our rubber duck
is flying! Which makes no sense, what would you do to solve this issue?
Of course, we can override
the fly()
method in our rubber duck and make it can not fly, but is it efficient in our future development?
What if in the future we are going to introduce more non-flyable duck types, wooden duck
, robotic duck
, should we override the fly()
method one by one?
Here comes our first design principle.
Identify the aspects of your application that vary and separate them from what stays the same.
This is basically saying take the part that would change a lot in the system and make it independent. In our case, since fly
is a method that going to vary a lot in our different duck types, we can take it out and make it a separate class
and for flyable ducks, they would inherit this class if needed.
Hold on for a minute and reflect on the design, is everything right?
If we have a concrete implementation of the fly method in a separate Flyable
class, then every subclass will have the same fly behaviour, what if there are other ducks fly in a different way?
To solve the above issue there comes our second principle.
Program to an interface, not an implementation.
The point is to exploit polymorphism by programming to a supertype so that the actual runtime object isn’t locked into the code.
With the above principles, we can have something like this,
class Duck(ABC):
def __init__(self):
pass
@abstractmethod
def display(self):
pass
@abstractmethod
def swim(self):
pass
@abstractmethod
def quack(self):
pass
class Flyable(ABC):
@abstractmethod
def fly(self):
pass
class RedheadDuck(Duck, Flyable):
def swim(self):
return "paddling"
def quack(self):
return "basic quack"
def fly(self):
return "fly with wings"
def display(self):
return "redhead duck"
# now rubber duck does not need to fly
class RubberDuck(Duck):
def display(self):
return "rubber duck"
def swim(self):
return "floating"
def quack(self):
return "jioooo"
Now the fly()
method is separated away with a Flyable
class, and for each of the subclass it needs to implement its own fly method, and, of course, for non-flyable type (our rubber duck
), it does not need to implement the Flyable
.
So far so good, but troubles never stop. Here comes a new requirement again, we have a bunch of other new ducks, bluehead, purplehead, yellowhead, …
ducks, all of them need to fly and they all fly in the same way. By using our current design, we would have things like this,
class RedheadDuck(Duck, Flyable):
## omit other methods ...
def fly(self):
return "fly with wings"
def display(self):
return "redhead duck"
class BlueheadDuck(Duck, Flyable):
...
def fly(self):
return "fly with wings"
def display(self):
return "bluehead duck"
# ... and many more ducks fly in the same way
How does this look to you now? It seems clear that we can reuse the code in the fly
, but however, we are not, but copy-pasting the same code everywhere.
How do we resolve this problem and make code reusable?
Now recall our definition of strategy pattern at the beginning.
The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Can we have a family of classes that implements the Flyable
interface and each of them has different fly behaviour which is reusable in the future?
class Flyable(ABC):
@abstractmethod
def fly(self):
pass
class Duck(ABC):
@abstractmethod
def display(self):
pass
@abstractmethod
def swim(self):
pass
@abstractmethod
def quack(self):
pass
class FlyWithWings(Flyable):
def fly(self):
return "fly with wings"
class FlyWithRockets(Flyable):
def fly(self):
return "fly with rockets"
class RedheadDuck(Duck, FlyWithWings):
def swim(self):
return "paddling"
def quack(self):
return "basic quack"
def display(self):
return "redhead duck"
# now objects can REUSE the fly method
class BlueheadDuck(Duck, FlyWithRockets):
def swim(self):
return "paddling"
def quack(self):
return "basic quack"
def display(self):
return "blue head duck"
Now for every duck that needs to fly with wings
can inherit the FlyWithWings class and with code reusable and without extra burden (each of the subclasses just needs to inherit what is needed).
It is all good now, and we’ve satisfied most of our requirements and changes should be easy to add into our current design, for example, if we need to introduce a backflip
functionalities into some of our duck types, we can add a BackFlip
behaviour interface and introduce of a family of classes implement it, with no overhead to our current system.
So is inheritance the best for code reuse?
What if our Fly
classes also have a bunch of functionalities that are redundant for our subclass, inherit the Fly
behaviour class would inherit all the redundant. Can we have the flexibility while removing the redundancy?
Favor composition over inheritance.
With composition instead of inheritance, you can encapsulate the methods that you like instead of inheriting all of them.
Let’s see how it is implemented (full code)
class Flyable(ABC):
@abstractmethod
def fly(self):
pass
class Duck(ABC):
def __init__(self):
self.flyable: Flyable = None
def performFly(self):
return self.flyable.fly()
def setFlyBehaviour(self, fly_behaviour: Flyable):
self.flyable = fly_behaviour
class FlyWithWings(Flyable):
def fly(self):
return "fly with wings"
class FlyWithRockets(Flyable):
def fly(self):
return "fly with rockets"
class RedheadDuck(Duck):
def __init__(self):
super().__init__()
self.flyable = FlyWithWings()
def display(self):
return "redhead duck"
We introduced a variable self.flyable
in the superclass and the actual fly behaviour is implemented in the subclass,
self.flyable = FlyWithWings()
And this part is called composition, where instead of inheriting, our redhead duck
is composed with the FlyWithWings
behaviour and this behaviour is able to change at runtime with the performFly
method.
What an amazing design we have achieved here! What we have done (full code),
Fly
method that changes from the original class.Flyable
interface and made a collection of classes implements it, each encapsulates different flying behaviour.
The strategy pattern really helps in providing flexibility to changes and makes the code reusable.
Reference
[1] https://github.com/ksatria/MK-Design-Pattern/blob/master/Ebook/Head First Design Patterns.pdf