Throughout the course of my career, I have worked at various companies ranging from the small startup to the massive tech company. The variety of my experience has led me to some conclusion especially when it comes to on-boarding with new projects and technology stacks. Software Design really matters. In addition to system architecture, algorithmic design, software design is a skill set in itself. You can have well designed and elegant algorithms but if the encapsulation logic and design around them is lacking, maintaining and on-boarding for new developers becomes a nightmare. As I am currently in social isolation (as most of us), I have decided to write articles about this field of study to pass the time and sharpen my own skills. For more in depth knowledge, I recommend "Design Patterns: Elements of Reusable Object-Oriented Software". This will be a brief overview to help my own recollection and hopefully help others through my struggles. A basic understanding of Object Oriented Design is recommended. It is also noted that these patterns are not silver bullets and based on your use case, can be considered over engineered solutions. I will be using Python to present examples, however, design patterns tend to transcend languages so you can follow along in your preferred language. So, with that introduction, let's explore the Strategy pattern.
A grave threat is upon our world! The only thing that stands between us and total destruction is YOU! You have been tasked to assemble a special team of heroes to combat this threat.
Let's define our base hero and give him some abilities.
class SuperHero: def display(self): raise NotImplementedError def attack(self): print("Punch!") def shield(self): print("Shield!") def fly(self): print("fly!")
Sweet we have a base hero that our heroes can inherit from! Now, let's create our first hero!
class IronMan(SuperHero): def display(self): print("I am Ironman!")
We just heard back from HQ, another hero has been found on ice. Lets create a class for him!
class SuperSoldier(SuperHero): def display(self): print("Captain America! Avengers Assemble!")
There is however a slight problem, the super soldier can't fly! Also, come to think of it, I think IronMan definitely has way better attacks than punch. Guess we can override those particular abilities.
class IronMan(SuperHero): def display(self): print("I am Ironman!") def attack(self): print("tank missile!") class SuperSoldier(SuperHero): def display(self): print("Captain America! Avengers Assemble!") def fly(self): print("I cannot fly...")
Do you see any issues with the above approach? What if more heroes can't fly? We'll have to duplicate the logic in
SuperSoldier.fly() violating DRY principle. Also, what if other heroes have attack abilities outside of punch? We'll have to override for each one and loose the value of inheritance. Additionally, one change to the super class can break functionality for old heroes. Is there a better way?
The first thing we want to do is identify what changes between objects and try and encapsulate that away into its own class of objects. We've identified
fly as two methods that vary between objects. So, lets create a contract or interface for those two methods.
class AttackAbility: def attack(self): raise NotImplementedError class FlyAbility: def fly(self): raise NotImplementedError
Now that we've established out contract or interface, how can we change our
SuperHero class to take advantage of this interface?
Program to an interface, not a implementation.
We know that our
FlyAbility has a
fly method. As the client of the interface, we don't really care how fly is implemented but rather just the contract is fulfilled and the function is called correctly.
class SuperHero: _fly_ability = FlyAbility() def fly(self): self._fly_ability.fly()
Of course, running this will result in a
NotImplementedError being thrown. So, we have to create an object that implements our fly ability for Ironman.
class RocketFlyAbility(FlyAbility): def fly(self): print("Rocket Thrusters engage") class IronMan(SuperHero): _fly_ability = RocketFlyAbility() def display(self): print("I am Ironman!") def attack(self): print("tank missile!")
What about the SuperSoldier who can't fly? Once again a new object to encapsulate that logic.
class NoFlyAbility(FlyAbility): print("Aww man, I can't fly...") class SuperSoldier(SuperHero): _fly_ability = NoFlyAbility() def display(self): print("Captain America! Avengers Assemble!")
Now lets introduce a new hero into the fold to see how easy it is to extend new heroes.
class Hulk(SuperHero): _fly_ability = NoFlyAbility() def display(self): print("Hulk Smash!!")
As homework, implement the attack methods in a similar way!
With a strategy like this, the planet should be in safe hands! That is until the next crisis!
The strategy patterns allows us to encapsulate implementations that vary between object to object. We also get a collection or hierarchy of similar algorithms and methods grouped under a superclass. Everything that inherits from
FlyAbility is guaranteed to have a
fly() method. The implementation is decoupled from the caller. This means we can change and add implementations without breaking existing functionality. What if a new Rocket comes out with faster flying enabled? Swap out Ironman's
_fly_ability. In fact, in future lessons, we will look at how to do this in a more dynamic fashion at runtime.