In the above UML class diagram, the Concrete class depends on an abstraction, Strategy interface. It doesn’t implement the algorithm directly. The Context from its method runStraegy calls the doAlgorithm in the Strategy concretion passed to it. The Context class is independent of the method and doesn't know and doesn't need to know how the doAlgorithm method is implemented. By virtue of Design by Contract , the class implementing the Strategy interface must implement the doAlgorithm method.

In strategy design pattern, there are three main entities: Context, Strategy, and ConcreteStrategy.

The Context is the body composing the concrete strategies where they play out their roles.

Strategy is the template that defines how all startegies must be configured.

ConcreteStrategy is the implementation of the Strategy template(interface).

Examples

Using Steve Fenton’s example Car Wash program , you know car wash can run on different grades of washing and cleaning depending on the money the driver has, the more the money the higher the wash level. Let's the Car Wash offers:

Basic Wheel and Body washing

Executive Wheel and Body washing

The Basic wheel and Body cleaning is just the normal soaping and rinsing for the body and brushing for the car.

Executive cleaning goes beyond that, they wax the body and the wheel to make it look shiny and then dry them. The cleaning depends on the level the driver pays for. level 1 gives you Basic cleaning for both body and wheels:

interface BodyCleaning {

clean(): void;

} interface WheelCleaning {

clean(): void;

} class BasicBodyCleaningFactory implements BodyCleaning {

clean() {

log("Soap Car")

log("Rinse Car")

}

} class ExecutiveBodyCleaningFactory implements BodyCleaning {

clean() {

log("Wax Car")

log("Blow-Dry Car")

}

} class BasicWheelCleaningFactory implements BodyCleaning {

clean() {

log("Soap Wheel")

log("Rinse wheel")

}

} class ExecutiveWheelCleaningFactory implements BodyCleaning {

clean() {

log("Brush Wheel")

log("Dry Wheel")

}

} class CarWash {

washCar(washLevel: Number) {

switch(washLevel) {

case 1:

new BasicBodyCleaningFactory().clean()

new BasicWheelCleaningFactory().clean()

break;

case 2:

new BasicBodyCleaningFactory().clean()

new ExecutiveWheelCleaningFactory().clean()

break;

case 3:

new ExecutiveBodyCleaningFactory().clean()

new ExecutiveWheelCleaningFactory().clean()

break;

}

}

}

You see now, some pattern is emerging. We are reusing the same class in many conditions, the classes are related but differ in behavior. Also, our code is getting untidy and heavy.

Most importantly, this our program fails the Open-Closed Principle in the S.O.L.I.D principles, which states that modules should be open for extension not modification .

For every new wash level, another conditional is added, that’s modification.

Using the strategy pattern, we will have to relieve our CarWash program of any responsibility for our knowledge of water level.

To do that we have to separate the cleaning actions. First, we create an interface all actions must implement:

interface ValetFaactory {

getWheelCleaning();

getBodyCleaning();

}

Then all the cleaning strategies:

class BronzeWashFactory implements ValetFactory {

getWheelCleaning() {

return new BasicWheelCleaning();

}

getBodyCleaning() {

return new BasicBodyCleaning();

}

} class SilverWashFactory implements ValetFactory {

getWheelCleaning() {

return new BasicWheelCleaning();

}

getBodyCleaning() {

return new ExecutiveBodyCleaning();

}

} class GoldWashFactory implements ValetFactory {

getWheelCleaning() {

return new ExecutiveWheelCleaning();

}

getBodyCleaning() {

return new ExecutiveBodyCleaning();

}

}

Next, we touch the CarWash program:

// ...

class CarWashProgram {

constructor(private cleaningFactory: ValetFactory) {

} runWash() {

const wheelWash = this.cleaningFactory.getWheelCleaning();

wheelWash.cleanWheels();



const bodyWash = this.cleaningFactory.getBodyCleaning();

bodyWash.cleanBody();

}

}

Now, we pass any cleaning strategy we want to the CarWashProgram.

// ...

const carWash = new CarWashProgram(new GoldWashFactory())

carWash.runWash() const carWash = new CarWashProgram(new BronzeWashFactory())

carWash.runWash()

Another Example: Authentication Strategy

Let’s say we have an app, that we want to secure ie add authentication to it. We have different auth schemes and strategies:

Basic

Digest

OpenID

OAuth

We might try to implement something like this:

class BasicAuth {}

class DigestAuth {}

class OpenIDAuth {}

class OAuth {} class AuthProgram {

runProgram(authStrategy:any, ...) {

this.authenticate(authStrategy)

// ...

}

authenticate(authStrategy:any) {

switch(authStrategy) {

if(authStrategy == "basic")

useBasic()

if(authStrategy == "digest")

useDigest()

if(authStrategy == "openid")

useOpenID()

if(authStrategy == "oauth")

useOAuth()

}

}

}

The same old long chain of conditionals. Also, if we want to auth. for a particular route in our program, we will find ourselves with the same thing.

class AuthProgram {

route(path:string, authStyle: any) {

this.authenticate(authStyle)

// ...

}

}

If we apply the strategy design pattern here, we will create an interface that all auth strategies must implement:

interface AuthStrategy {

auth(): void;

} class Auth0 implements AuthStrategy {

auth() {

log('Authenticating using Auth0 Strategy')

}

} class Basic implements AuthStrategy {

auth() {

log('Authenticating using Basic Strategy')

}

} class OpenID implements AuthStrategy {

auth() {

log('Authenticating using OpenID Strategy')

}

}

The AuthStrategy defines the template by which all strategies must build on. Any concrete auth strategy must implement the auth method to provide us with its style of authentication. We have the Auth0, Basic and OpenID concrete strategies.

Next, we need to touch our AuthProgram class:

// ...

class AuthProgram {

private _strategy: AuthStrategy

use(strategy: AuthStrategy) {

this._strategy = strategy

return this

}

authenticate() {

if(this._strategy == null) {

log("No Authentication Strategy set.")

}

this._strategy.auth()

}

route(path: string, strategy: AuthStrategy) {

this._strategy = strategy

this.authenticate()

return this

}

}

You see now, the authenticate method doesn’t carry the long switch case. The use method sets the authentication strategy to use and the authenticate method just calls the auth method. It cares less about how the AuthStrategy implements its authentication.

log(new AuthProgram().use(new OpenID()).authenticate())

// Authenticating using OpenID Strategy

Strategy Pattern: Problems It Solves

Strategy Pattern prevents hard-wiring of all the algorithms into the program. This makes our program complex and much more bogus and hard to refactor/maintain and understand.

This, in turn, makes our program to contain algorithms they do not use.

Let’s say we have a Printer class that prints in different flavors and style. If we contain all the styles and flavors of printing into the Printer class:

class Document {...} class Printer {

print(doc: Document, printStyle: Number) {

if(printStyle == 0 /* color printing*/) {

// ...

}

if(printStyle == 1 /* black and white printing*/) {

// ...

}

if(printStyle == 2 /* sepia color printing*/) {

// ...

}

if(printStyle == 3 /* hue color printing*/) {

// ...

}

if(printStyle == 4 /* oil printing*/) {

// ...

}

// ...

}

}

OR

class Document {...} class Printer {

print(doc: Document, printStyle: Number) {

switch(printStyle) {

case 0 /* color priniting strategy*/:

ColorPrinting()

break;

case 0 /* color priniting strategy*/:

InvertedColorPrinting()

break;

// ...

}

// ...

}

}

You see we end up with a bogus class, that is hard to read, maintain and with too many conditionals.

But with the Strategy Pattern, we break the printing styles into different tasks.

class Document {...} interface PrintingStrategy {

printStrategy(d: Document): void;

} class ColorPrintingStrategy implements PrintingStrategy {

printStrategy(doc: Document) {

log("Color Printing")

// ...

}

} class InvertedColorPrintingStrategy implements PrintingStrategy {

printStrategy(doc: Document) {

log("Inverted Color Printing")

// ...

}

} class Printer {

private printingStrategy: PrintingStrategy

print(doc: Document) {

this.printingStrategy.printStrategy(doc)

}

}

So, instead of many conditionals, each condition is moved to a separate strategy class. There is no need for the Printer class to know the different printing styles implementation.

Strategy Pattern and the SOLID Principles

In Strategy Pattern, composition is used over inheritance. It is advised to program to abstraction than to concretions. You see that Strategy Pattern is compatible with the SOLID principles.

As an example, we have a DoorProgram that have different styles of locking mechanism to lock doors. As different locking mechanisms change between subclasses of door. We might be tempted to apply the door locking mechanism to the Door class like this:

class Door {

open() {

log('Opening Door')

// ...

} lock() {

log('Locking Door')

} lockingMechanism() {

// card swipe

// thumbprint

// padlock

// bolt

// retina scanner

// password

}

}

It seems OK, but the behaviours of doors differs. Each has its own locking and opening mechanism. That is different behaviours.

When we create different types of Doors:

// ...

class TimedDoor extends Door {

open() {

super.open()

}

}

And try to implement it the open/lock-ing mechanism, you see that we must call the parent method before implementing its own open/lock mechanism.

If we make the Door an interface like this:

interface Door {

open()

lock()

}

You see that the open/lock behavior must be declared in each class or model or types of Door.

class GlassDoor implements Door {

open() {

// ...

}

lock() {

// ...

}

}

Quite good, but there are many drawbacks here which will pop up as our app grows. A Door model must have an open/lock mech. Is it a must a Door must open/close? No. A Door might not even be closed at all. So we see our Door models will be forced to open/lock.

Next, the interface doesn't draw a line between using the interface as a model or as an open/lock mech. Note: in S in SOLID a class must have one responsibility.

A Glass Door must have the only characteristics of a Glass Door also a Wooden Door, a Metal Door, a Ceramic Door(do they have that?) Another class should be responsible for handling the opening/locking mechanism.

Using SP, we separate our related, in this case, the locking/opening mech. into classes. Then at runtime, we pass the Door model the lock/open mechanism it is to use. The Door model can select from a pool of lock/open strategies which lock/open mech. to use.

interface LockOpenStrategy {

open();

lock();

} class RetinaScannerLockOpenStrategy implements LockOpenStrategy {

open() {

//...

}

lock() {

//...

}

} class KeypadLockOpenStrategy implements LockOpenStrategy {

open() {

if(password != "nnamdi_chidume"){

log("Entry Denied")

return

}

//...

}

lock() {

//...

}

} abstract class Door {

public lockOpenStrategy: LockOpenStrategy

} class GlassDoor extends Door {} class MetalDoor extends Door {} class DoorAdapter {

openDoor(d: Door) {

d.lockOpenStrategy.open()

}

}

const glassDoor = new GlassDoor()

glassDoor.lockOpenStrategy = new RetinaScannerLockOpenStrategy(); const metalDoor = new MetalDoor()

metalDoor.lockOpenStrategy = new KeypadLockOpenStrategy(); new DoorAdapter().openDoor(glassDoor)

new DoorAdapter().openDoor(metalDoor)

Each open/lock strategy is defined in a class inheriting from a base interface. SP supports this because it is better to code to an interface so as to achieve high cohesion.

Next, we have our Door models each a subclass of the Door class. We have a DoorAdapter whose job is to open doors passed to it. We created objects of a couple of Door models and set their lock/open strategies. The glass door is to be locked/opened via retina scanning and the metal door has a keypad for entering the secret password.

The thing we achieved here is the separation of concerns, separation of related behaviors. Each Door models doesn’t know and bear the concern of implementing a certain locking/opening strategy, it was delegated to another entity. We programmed to an interface as required by SP because it makes switching strategies during runtime easy.

This might not hold for long but it is a better approach courtesy of the Strategy Pattern.

A Door might have many lock/open strategies and might use one or all during both locking and opening. Whatever you do keep the Strategy Pattern in mind.

Strategy Pattern in JavaScript

Most of our examples are based on OOP languages. JS isn’t statically typed but dynamically typed. So there is no concept of the OOP like interface, polymorphism, encapsulation, delegation is not present. But in SP, we can assume they are present, we simulate them.

Let’s use our first example to demonstrate how we could apply SP in JS.

The first example was based on sorting algorithms. Now, the interface SortingStrategy has a method sort that all implementing strategies must define. The SortingProgram class takes a SortingStrategy in its runSort method and calls the sort method.

We model our sorting algorithms:

var HeapSort = function() {

this.sort(array) {

log("HeapSort algorithm")

// implementation here

}

} // linear search sorting algorithm implementing its alogrithm in the `sort` method

var LinearSearch = function() {

this.sort(array) {

log("LinearSearch algorithm")

// implementation here

}

}

class SortingProgram {

constructor(array) {

this.array=array

}

runSort(sortingStrategy) {

return sortingStrategy.sort(this.array)

}

} // instantiate the `SortingProgram` with an array of numbers

const sortProgram = new SortingProgram([9,2,5,3,8,4,1,8,0,3]) // sort using heap sort

sortProgram.runSort(new HeapSort()) // sort using linear search

sortProgram.runSort(new LinearSearch())

There was no interface but yet we did it. There could be a better and robust way but for now, this will suffice.

The thing here is to have it in my mind that for every sorting strategy we want to implement it must have a sort method where the sorting will be carried.

Strategy Pattern: When To Use

Strategy Pattern should be used when you begin to notice recurring algorithms but in different variations. This way, you need to separate the algorithms into classes and feed them based on want in your program.

Next, if you notice recurring conditional statements around a related algorithm.

When most of your classes have related behaviors. It will be time to move them into classes.

Advantages

Separation of Concerns: Related behaviors and algorithms are separated into classes and strategies.

Easy switching of strategies in runtime because you always program to interfaces.

Elimination of bogus and conditional-infested code.

Easy maintainability and refactoring.

Choice of algorithms to use.

Conclusion

Strategy Pattern is one of the many Design Patterns in software development. In this post, we saw many examples of how to use the SP and later on, we saw its benefits and drawbacks.

Remember, you don’t have to implement a design pattern as described. You have to thoroughly understand it and know when to apply it. And if you don’t understand it, no worry, keep referring to it again and again for insights. With time you’ll get the hang of it, and in the end, you will see the benefits.

Next, in our series, we will be looking into the Template Method Design Pattern so stay tuned :)

If you have any question regarding this or anything I should add, correct or remove, feel free to comment, email or DM me. Thanks for reading! 👏

Credits