Strategy Design Pattern
9 min read
Photo by La-Rel Easter on Unsplash
We discussed what are design patterns and why we need them in my previous article. We are going to dive head first into one of the most commonly used design patterns - Strategy Pattern. You might have used it or a variation of it already in your day to day life.
To better understand this design pattern, we will try finding answers to the following questions.
- What problem does it solve?
- What is Strategy Pattern ?
- When to use Strategy Pattern ?
- When not to use Strategy Pattern ?
To truly understand something and to remember it forever, we need to understand all aspects of a topic. Memorizing a concept with a term will only last awhile.
If I had an hour to solve a problem, I'd spend 55 minutes thinking about the problem and 5 minutes thinking about the solution.
- Albert Einstein
Let's imagine you got a job at a big game developing studio. The studio is building the next Justice League game. On your first day, you are given the task to design the class structure for the core superheroes. In one of your daily stand-ups the solution architect also told you that there might be a possibility in near future that the studio might add a new feature which allows the users to create their own super hero from scratch.
Now, this is a big task and can get very complicated as Justice League heroes range from Batman to Superman and everything in between. But for sake of this article let's keep it simple.
You are sitting at your desk and thinking about the implementation. Every hero will have its own character based on the things he/she can do, his/her capabilities and his/her weakness. So you think about it and the simplest way to do it, is to have a generic parent class, which defines functions representing hero behavior. Then every hero can inherit this class and then implement their own behaviors. You are using one of the basic concepts of Object oriented programming - Inheritance.
You have finally used the object oriented principles you learned back in school. This looks good for now, but let's say this was approved and pushed to production. We designed the system with just 2 heroes and the gamers are not too happy with it, because the game does not include the third founding father of Justice League, The Batman. You realize your mistake and rush to implement. But this time you are extra careful and add more heroes to the system.
You made it to the production just in time. If you observe the design one thing sticks out, that is not desirable. Let's look at the class of Green Arrow. The class implements all the methods of its parent class. As you know Green Arrow can't fly so the fly() method in Green Arrow will do nothing. Same goes with the Batman class. Actually if you think about it, both Batman and Green Arrow don't fly but prefer running or driving a car. For argument sake let's say both Green Arrow and Batman prefer running instead of flying. So the solution would be to copy paste the same behavior in both green arrow and batman class.
We can summarize some of the pain points from this design.
- We have one Parent class and based on current design we have to write a new class every-time we want to add a new super hero.
- Any changes in parent class for example a new method means we need to write more code for all the classes inheriting it.
- Multiple classes (Green Arrow, Batman) do not implement fly() method but they have a totally different behavior.
- We are repeating code for Green Arrow and Batman for their run behavior.
Strategy pattern exists to solve the above problems. Our first implementation is not wrong from object oriented programming perspective, but we can optimize it for scalability and maintainability.
Before jumping straight into strategy pattern, let's try to refresh some of the important concepts, and how we can use them to our advantage.
Composition over Inheritance.
In general inheritance is not meant for code re-use. It has its advantages but in the context of the problem we are trying to solve, inheritance is not helping much.
Inheritance allows us to have Is-a relationship. Wonder woman extends the class Hero, so by definition, wonder woman Is-a hero and it inherits everything from the class Hero.
Now if we look at the concept of composition, it describes a class that references one or more other objects of other classes in instance variables. This allows us to have has-a relationship between objects. It is important to understand the difference between Inheritance and Composition.
We can find real world examples, like for example, a car and an engine. A car has-a engine. Both these entities are different and independent of each other.
Interfaces by definition are blueprints for the functionality contained in associated object. You can think of interface like a contract, it ensures that the implementing class satisfies the terms in the contract. Here is a simple example.
You might wonder, why am I talking about interfaces all of a sudden. What role do they play ?
It turns out that they are going to play a big role. Think about the business logic. When we design a system, it can have a fixed business logic or a variable one, completely dependent on the context of the project. For example, if you are designing your own e-commerce website then the logic for calculating sales tax for the state of New York will be a fixed business logic. Although if you are selling nation wide in all 50 states then the logic becomes variable, because every state will have its own sales tax calculation logic.
In our example, if you think about it, every superhero even though they have super-powers, they are completely different from one another. We cannot generalize them under one fixed logic. The business logic for implementing superpower behavior in our code should be a variable one. Interfaces are going to help us achieve this.
We discussed about Inheritance, Composition and Interfaces. Now, let's see how they can solve the problem in hand.
In Strategy pattern we define a family of algorithms , encapsulate each one and make them interchangeable during run time. The algorithms we define change independently from the client that uses it.
If the above definition adds more to the confusion, then don't worry we will walk through the actual implementation and then revisit this definition.
In our project, every super hero has some generalized behavior. Why not use the concept of Composition and Programming to Interface here ? Let's see how to implement that.
We can extract out every behavior from parent class and make that as an interface. Specific hero behavior then can be programmed to implement that interface.
This solves our problem with the Batman and Green Arrow class. We no longer have to repeat the same code in both the classes. We can just program the NoFly() class to accommodate the no flying behavior of Batman and Green Arrow.
What if we encounter a hero who does not run, does not fly (for argument sake, I know he can fly) but swims instead. You guessed it right what about out poor old Aquaman. Its simple we can add one more class which will implement the IFlyBehavior interface. Now this interface looks good and kind of solves our problem, but how exactly are we going to tie all this up. This is where the composition comes into picture.
Remember the parent class, yeah that class which we programmed on our very first day at the job. Time to make some changes to that. I know you have good memories attached to that but hey its time to refactor.
Parent class is where we can have composition. We should have instance of every behavior interface initialized in this class. The methods in this class can then delegate it to the actual behavior implementation. Too much information ? let's take a look at the code below.
The constructor in SuperHero class accepts a behavior instance which can be changed as per need during the run time. This gives us flexibility to pick and chose behaviors for our super heroes.
If we want to implement more heroes with different behaviors, then all we need to do is add more classes which implement the behavior interface and in our program we can pick and chose which behavior we want. It's that simple.
Let's see if we were able to solve the problems we had before.
- We don't need to add a new class for every new super hero. We only add new classes for new behaviors.
- We no longer use inheritance.
- We have NoFlyBehavior for superheroes that do not fly.
- We have one class which we can use for similar behaviors, so we don't need to repeat the code.
We can now say that our design is good and long lasting. It is resilient enough to be extended for future purposes without affecting the existing code.
If we go back to the definition, and look at it again, it makes more sense now. The family of algorithms are the concrete implementations of the behavior like SuperStrength, LassoOfTruth, NoFly and FlyInAir classes. These algorithms are interchangeable at run time as they are programmed to the interface. And lastly the algorithms can change independently with respect to the client which in this case is the SuperHero class.
When to use it ?
Strategy Pattern should always be used whenever there is a need to change the implementation during run time. Like for example in our case, each hero had a different algorithm for the same behavior.
Use Strategy Pattern when you have a lot of similar looking classes but they differ slightly in their behavior. This will help you extract only varying behaviors in separate classes and just one class each for similar behaviors.
When not to use it ?
If you have only couple of behaviors and you are sure that no new behavior will be added, then I think that the strategy pattern will be an overkill. If the game strictly had just 2 super heroes and did not plan to release anything new, then implementing those 2 super heroes with inheritance is the best way to go . After-all remember KISS (keep-it-simple-stupid).
One of the important things that we did not discuss, is that the client (SuperHeroSimulator) class has to know which algorithm to implement during run time. This can become a bottle neck in very specific scenarios. You need to understand your project's context and decide for yourself if strategy design pattern is a good idea.
The best way to understand and become adept at using Design Patterns is to carefully assess the need of your project and of course by actually implementing it.
We are all here to learn something new every day, so if you find any mistakes or have a better example please reach out. I would be very happy to discuss and start a conversation.