The strategy pattern is used to encapsulate functionality and maximize maintainability while exercising polymorphism and inheritance.
Simple inheritance example:
Given an abstract superclass Feline
; We have two concrete implementations HouseCat
and WildCat
. Each of the classes inherit the walk()
functionality, and implement their own meow()
functionality:
// Feline superclass public abstract class Feline { void meow(); void walk() { // do walk } } public class HouseCat implements Feline { @Override public void meow() { // do soft meow } } public class WildCat implements Feline { @Override public void meow() { // do meow with growl } } |
Problem: Add new functionality to Feline:
Let’s say we want to add some functionality to the Feline
s so they can get food. Thinking in an Object Oriented mindset the easiest solution would be to add a new method getFood()
to the Feline
superclass. This way all the subclasses of Feline
will inherit the functionality.
public abstract class Feline { void meow(); void walk() { // do walk } // new method to get food void getFood(); } |
On the surface this appears to be a good solution to the problem. But what if some of the existing Feline
subclasses aren’t supposed to getFood()
? What if other subclasses are added that shouldn’t getFood()
or walk()
? Now we have duplicated code across classes and empty implementations. Suddenly using inheritance for reuse has made maintenance difficult!
Solution – Utilize the Strategy Pattern
Let’s start by creating some interfaces
which cover the different behaviors Feline
s have.
Meow Behavior:
// interface public interface MeowBehavior { void meow(); } // concrete implementations public class MeowSoftly implements MeowBehavior { @Override public void meow() { System.out.println("meow..."); } } public class MeowWithGrowl implements MeowBehavior { @Override public void meow() { System.out.println("urrrrrrrMEOW"); } } |
Eat Behavior:
// interface public interface GetFoodBehavior { void getFood(); } // concrete implementations public class GetProvidedFood implements GetFoodBehavior { @Override public void getFood() { // eat food from dish } } public class HuntForFood implements GetFoodBehavior { @Override public void getFood() { // hunt for food in the wilderness } } |
Now that we have some behaviors defined the functionality is no longer encapsulated within the Feline
object and can be reused by other types of objects. Let’s upgrade the Feline
class to utilize the Strategy Pattern.
Let’s add two new instance variables to the Feline
class and delegate the existing meow()
and getFood()
methods out to the new behavior interfaces:
public abstract class Feline { // add the new behaviors as instance variables MeowBehavior meowBehavior; GetFoodBehavior getFoodBehavior; // delegate the behavior out to the behavior interfaces void meow() { meowBehavior.meow(); } void getFood() { getFoodBehavior.getFood(); } } |
Now we have encapsulated our meow()
and getFood()
functionality outside of the Feline
class! Let’s spin up a new HouseCat
/ WildCat
and implement the desired behavior using polymorphism.
public class HouseCat extends Feline { public HouseCat() { meowBehavior = new MeowSoftly(); getFoodBehavior = new GetProvidedFood(); } } public class WildCat extends Feline { public void WildCat() { meowBehavior = new MeowWithGrowl(); getFoodBehavior = new HuntForFood(); } } |
Modify Feline Behaviors at Runtime
Another advantage to using the Strategy Pattern is exposing the ability to modify an objects behavior at runtime. This wasn’t possible using the simple inheritance example. We can achieve this by exposing a behavior setter
in the Feline
class:
public abstract class Feline { // new methods to dynamically alter behavior at runtime void setMeowBehavior(MeowBehavior meowBehavior); void setGetFoodBehavior(GetFoodBehavior getFoodBehavior); } |
Implementation Example:
Now we have everything we need to implement our different cats using the Strategy Pattern. New functionality can be added at anytime without modifying any of our Behavior classes or the Feline
superclass.
// Cats Feline houseCat = new HouseCat(); Feline wildCat = new WildCat(); houseCat.meow(); // observe output -> "meow" wildCat.meow(); // observe output -> "urrrrrrrMEOW" // modify the meow behavior at runtime houseCat.setMeowBehavior(new MeowWithGrowl()); wildCat.setMeowBehavior(new MeowSoftly()); houseCat.meow(); // observe output -> "urrrrrrrMEOW" wildCat.meow(); // observe output -> "meow" |
Outstanding Question:
While I feel I have a good understanding of the Strategy Design Pattern one thing sticks out. What if a new subclass of Feline
is written but the developer doesn’t assign behavior implementations to the instance variables? What’s the best solution for this?
- Create a default behavior and assign it in the abstract superclass?
- Add a construct which forces developers to pass in behaviors at initialization?
- Throw an exception if no assignment is made?