Strategy Pattern Definition and Contrived Example

Disclaimer

Like everything patterns should only be used where appropriate. Using patterns where they are not suited often causes more headaches/problems than not using any at all.

The strategy pattern encapsulates interchangeable algorithms normally consisting of volatile behaviours (things that are likely to change in the lifespan of the project). Using this, it becomes easy to swap the behavioural implementations (compile & runtime) without changing the code of the "owning" object.

A Contrived example

We have been asked to build a petting zoo simulator, with three animals (Duck, Sheep and Rabbit). The initial requirements state that the only functionality we need is that they make an appropriate noise. The following is how it could be implemented using standard inheritance.

A Contrived example

The Animal abstract class declares the sound property and the MakeNoise method implementation displays the noise to the user. Duck, Sheep and Rabbit inherit from Animal and declare the noise the animal will make.

So far so good and a few months go past with this code out in the wild. However the client has just been called and it appears people are becoming bored with animals that just make noises. They now want the sheep to be able to walk around.

Having a fairly good grasp of OOP principles we decide that Walk() does not belong to sheep but really to animal and decide to add the method there.

*We have made the mistake here of taking the clients requirements literally instead of abstracting them. Walk is a specific type of movement and so calling our method Move() will be more appropriate.

class diagram

Its hard to see it with this small subset of classes but in larger systems adding functionality to a parental class can have far reaching side effects that can do anything from crash the program to introducing hard to find bugs.

Let's say we had tunnel vision/limited domain knowledge when adding this feature and forgot about Duck and Rabbit. The client has called back and mentions that although extremely happy that the Rabbits and Ducks are moving, the Duck should waddle and the Rabbit should hop.

Okay, not a problem. We can just override the Rabbit and Duck Walk methods with their own specific implementations.

animal class

It's getting a little messy but still it's not too bad, the developer needs a little more domain knowledge to make changes and we now have methods that are not really named appropriately, for example calling Walk() on the Rabbit class will make the Rabbit hop. Ideally we would re-factor this now (especially as it is so small) but following the "don't fix what isn't broke" methodology we decide to leave it.

Due to extra competition in the petting zoo simulators it was decided to add more animals. The two chosen to lead the way are Kangaroo and Snake, but others will follow in the near future.

We add Snake fairly easily as in the following methodology.

previous methodology

But Kangaroo is proving to be a little more difficult. We want it to hop just like a Rabbit but it makes little to no sense for a Kangaroo to inherit from a Rabbit or vise versa. We could just invalidate DRY principles and create the new animal like so:

DRY principles

This isn't ideal, we have duplicated the code of Rabbits that hop and our code is becoming pretty opaque/confusing.

This seems like a good place to implement the Strategy pattern, we have the volatile behaviour Walk() and thinking about it Sound and MakeNoise() could also present similar problems in the future.

First of all let's make the abstract class Animal an interface. (This is not required by the strategy pattern but one of its key principles is composition over inheritance and the moment we extract the functionality it would behave just like an Interface).

This is also the perfect time to rename Walk() to Move(). We are making changes to its code so it will be required to re-test its functionality anyway. The new name will make more sense thus making it easier to read/maintain.

We will create two new interfaces IMove and IMakeNoise and declare them in IAnimal instead of the current Methods.

IMakeNoise

Now we need to implement our encapsulated behaviors and add them to our animals.

At this point it's worth pointing out that we should be programming to interfaces and not concrete implementations, especially in the code surrounding/using these classes as without doing this we loose the benefits of using this pattern.

concrete implementations

Of course any code change is not without its risks and its soon realised that Ducks have stopped waddling and started walking.

There is nothing to panic about, since we are programming to an interface we can create a new implementation of IMove and swap the implementation on our Duck class. We also notice our Kangaroo is no longer hopping. We better fix that too.

IMove

Not only have we seamlessly slotted new behavioural functionality into an existing class without changing any of its internals we have re-used the Hop code (keeping our code nice and dry).

Using this pattern we can also delegate decisions on what type of algorithm to implement right up until runtime. An example of this could be having two types of Rabbits, one hops and another walks. We can now in the desired behaviour at construction and as long as it implements the IMove interface everything will work as intended.

This code is starting to look pretty clean but we have one more "major" issue that is apparent. We are constantly creating implementations of concrete classes and when we want to make changes we need to make them to all relevant classes. For example, It has been decided that Kangaroo's do not hop but they bounce, we create a new Bounce class implementing the IMove interface() and need to go inside Kangaroo and swap Hop() with Move(). We need to make the change to the class itself. (Classes should be closed to modification but open to extension).

This is where other patterns can come to the rescue, for example the simple factory/factory method pattern or IoC but they are outside the scope of this article.

Like any pattern there are downsides and one of the easiest to spot with the Strategy pattern is the explosion of "Behavioural" classes/interfaces you have in your code base. Our final example has 7 extra but what if there is suddenly a requirement to add Eating functionality that is dependant on food types. This can create a cascade of new Interfaces and Classes that spiral out of control.

It is strongly recommended that you only use this pattern in highly volatile areas that are subject to frequent change.

I hope you have enjoyed this article and found it useful. All feedback including negative is welcome.

Many thanks
Les


Similar Articles