Strategy Design Pattern: Explained

Shubham Sharma
7 min readJan 31, 2023

--

Imagine that you are working in a company which requires you do design a simple game application for kids. This application involves simulating the behaviours and activities of different types of Animals. The general behaviours may include the animals’ appearances, the different kinds of sounds they make, how they rest. Thus, you come up with a basic OO (Object Oriented) solution.

Fig. 1

In Figure 1, an inheritance based solution has been employed to derive various concrete classes out of a base class Animal. All the concrete animal classes inherit from the abstract class Animal. The methods eat and makeSound have been defined in the abstract class Animal. This prevents code duplication in each of the concrete child classes. The display method is abstract and is overridden in each of the inherited classes as each animal looks different. At the initial look, this solution looks great.

But wait. Let’s say another requirement comes from the business side that now we will have a toy animal made of plastic too. However, this toy animal neither eats nor makes any sound. For this class to be placed in the old hierarchy, both the eat and makeSound methods need to be overridden

Fig. 2

If we analyse, our point of using inheritance was to reduce code duplication. However, every new child class may or may not implement the functions of the base class. Even if majority of them do, each might do it differently. For example, a tiger would eat only deer but a pigeon would eat only worms. So in this case, behaviour of eating is common but the inner details vary across the child classes. Thus, what we thought was a great use of inheritance for the purpose of reuse has not turned out to be that great when it comes to extending and maintaining our code for future use cases.

Let’s try with an interface

Inheritance is probably not the answer. The requirements from the business team might change every six months in ways which has not been decided before and the spec will keep changing. The only option you would have would be to keep on overriding eat() and makeSound() method in each of the Animal child classes. How to go about next?

You can probably use interfaces like CanEat or CanMakeSound.

Fig. 3

Although interfaces solve the problem of the plastic tiger not eating, not making a sound and not having to override the eat and makeSound methods in its class, the original point of using OO design to reduce code duplication does not hold now. Each implementation of pigeon or snake will have to separately define its own eat and makeSound method. This can be a maintenance nightmare. Once these methods are defined in the concrete subclasses, any change in spec ie change in eating or sound behaviour would mean change in the respective classes. This violates the second SOLID design principle ie a class should be open for extension and closed for modification.

Encapsulate what varies

There are behaviours of the Animal class which vary across each concrete implementation. These can include eating behaviour, sound etc. Other behaviours like rest do not require frequent change. Then why not encapsulate these changing behaviours into one interface. You can have distinct classes which represent different behaviours implementing from this common interface.

Fig. 4

Program to an interface, not an implementation

The whole point of this exercise is to keep things flexible and ensure that further addition of any new behaviour in the animal class does not lead to a change or method overriding in any of the already existing concrete classes of the animals. Thus, it is important for you to make sure that Animal class does not implement the interfaces FoodBehaviour or SoundBehaviour. Instead, there should be separate classes whose only point of existing is to represent a behaviour. These behaviour classes will implement the interface. Herbivore is class let’s say which implements the EatBehaviour interface. The core logic will be inside these behaviour classes. Thus, Animal class will have a class variable of the type EatBehaviour or SoundBehaviour. Any concrete class which implements these behaviours can be assigned to these variables. Because of polymorphism, all the behaviours can be assigned to the variable of type EatBehaviour.

Fig 5

The point is to exploit polymorphism by programming to a super type so that the actual runtime object isn’t locked into the code. The declaration type of the variables should be a super-type, usually an abstract class or interface, so that objects assigned to that variable can be of any concrete implementation of the super-type. Thus, classes declaring them doesn’t have to know about the actual object types.

Let’s understand this with the help of an example,

Programming to an implementation would mean

Dog d = new Dog();
d.bark();

Declaring the variable “d” as type Dog (a concrete implementation of Animal) forces us to code to concrete implementation

Animal animal = new Dog();
animal.markSound();

We know it is a dog, but we can use the animal reference polymorphically. In other words, we have now programmed to an interface(super-type) not to an implementation.

To do it even better, we can assign the concrete implementation at runtime rather than hardcoding it

Animal animal = getAnimal();
animal.makeSound();

We don’t know beforehand what concrete implementation of Animal will be assigned to the animal variable. But we know for sure that since the concrete implementation inherits or implements the Animal super-type (abstract class or interface), it knows how to handle makeSound.

Implementing the eat and sound behaviour

Similar to the EatBehaviour as shown in Fig 5 we can have a sound behaviour also with its different concrete implementations.

Fig 6

In case any new kind of EatBehaviour or SoundBehaviour needs to be incorporated in our application, simply we would create a new class which implements these behaviour interfaces. These behaviour implementations are no longer hidden in the Animal class. Thus, to add a new behaviour we do not have to modify any of our existing behaviours or touch the Animal class which uses that behaviour. Hence, we are able to satisfy our second SOLID principle that a class should be open for extension but closed for modification. Following is the java code which represents our classes.

public abstract class Animal {
EatBehaviour eatBehaviour;
SoundBehaviour soundBehaviour;
public Animal() {}
public void setEatBehaviour(EatBehaviour eatBehaviour) {
this.eatBehaviour = eatBehaviour;
}
public void setMakeSoundBehaviour(SoundBehaviour soundBehaviour) {
this.soundBehaviour = soundBehaviour;
}
public void eat() {
this.eatBehaviour.eat();
}
public void makeSound() {
this.soundBehaviour.makeSound();
}
public abstract void display();
public void rest() {
System.out.println("Animal is resting");
}
}
public interface EatBehaviour {
public void eat();
}
public interface SoundBehaviour {
public void makeSound();
}
public class EatWorms implements EatBehaviour {
public void eat() {
System.out.println("Eat Worms");
}
}
public class EatDeer implements EatBehaviour {
public void eat() {
System.out.println("Eat Deer");
}
}
public class EatRats implements EatBehaviour {
public void eat() {
System.out.println("Eat Rats");
}
}
public class Hiss implements SoundBehaviour {
public void makeSound() {
System.out.println("Hiss");
}
}
public class Coo implements SoundBehaviour {
public void makeSound() {
System.out.println("Coo");
}
}
public class Growl implements SoundBehaviour {
public void makeSound() {
System.out.println("Growl");
}
}

The above are the main classes used in our implementation. Each eat or sound behaviour is a concrete implementation of a generic interface. Animal contains a variable of the type generic interface and concrete implementations are flexibly assigned to the Animal class.

public class Tiger extends Animal {
public Tiger() {
this.setMakeSoundBehaviour(new Growl());
this.setEatBehaviour(new EatDeer());
}
}
public class Simulator {
public static void main(String[] args) {
Animal tiger = new Tiger();
tiger.makeSound();
tiger.eat();
tiger.setEatBehaviour(new EatRats());
tiger.eat();
}
}

The terminal shows output like as follows

Thus, using the above implementation we are not just able to add new eat or sound behaviours but also change the behaviours at runtime as nothing is hardcoded.

The Big Picture

The different ways of eating or making sound can also be generalised as family of algorithms. Our client makes use of an encapsulated family of algorithms for eating and making sound.

Fig 7

As we look closely at figure 7, we find that each animal has a HAS-A relationship with Eat Behaviour and Sound Behaviour. When two classes are put together, it is called a composition. So instead of one class inheriting the other class, animals get their behaviour by being composed with the right behaviour object. Composition gives us a lot of flexibility as helps us keeping the animal classes and the behaviour classes loosely-coupled. Moreover, it lets us change the behaviour class for an animal at runtime.

Give a pat on your back! For having shown the patience to complete the full article. We just used the Strategy design pattern which can be succinctly defined as:

The Strategy Pattern defines a family of algorithms, encapsulates each one and makes them interchangeable. Strategy lets clients vary independently from clients that use it.

Thanks for bearing with me and reading the full article. Please find the github link to the code https://github.com/abhirath14/StrategyDesignPattern

Special mention to the Head First Design patterns by Eric Freeman and Elisabeth Robson for helping me develop understanding of the subject.

--

--

Shubham Sharma
Shubham Sharma

No responses yet