Design Patterns: The Strategy Pattern

The formal definition of the Strategy Pattern is:

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

I really like the example shown in Head First Design Patterns to explain this pattern. The premise goes something like

  • Joe works for a company that makes a highly successful duck pond simulation game. it is called SimUDuck
  • The system architecture is something like this: A single Duck super class allows all other duck types to inherit from it. For example, MallardDuck, and RedheadDuck.

In pseudo-code (akin to TypeScript)

class Duck {
    public quack() { ... }
    public swim() { ... }
    public display() { ... }
}

class MallardDuck extends Duck {
    public display()
}

class RedheadDuck extends Duck {
    public display()
}

Joe needs a new feature for all Duck types. The feature happens to be the ability for ducks to fly.

What can we do? We can simply just add a fly method to Duck, and thus all classes inheriting from Duck will just fly, right?

class Duck {
    public quack() { ... }
    public swim() { ... }
    public display() { ... }
    public fly() { ... } // new method
}

class MallardDuck extends Duck {
    public display()
}

class RedheadDuck extends Duck {
    public display()
}

There is still a problem with this. What if Mary creates a RubberDuck which extends from Duck? A rubber ducky is still a duck, after all.

class RubberDuck extends Duck {
    // overridden to squeak
    public quack() {
        squeakInternal();
    }
    
    public display() { ... }
}

If Mary does that, then all of the sudden, with Joe’s change, a RubberDuck instance is allowed to fly! That is not correct. Joe has failed to notice that not all subclasses of Duck should be able to fly. When Joe had added a fly method to the Duck class, he made a dangerous assumption that all Duck types will be able to fly. This even includes inanimate ducks.

Joe has a quick solution to this! His reasoning now is that if RubberDuck cannot fly, then the solution is simple. He can just simply have Mary override the fly method in her RubberDuck class to be a no-op whenever its fly method is called.

Too bad Charlie needs to also implement a WoodenDuck. He finds it really inconvenient that he has to perform such an awkward implementation for this WoodenDuck which cannot fly.

Charlie tells Joe that his approach is not a solution, but a hack. Joe begins to think whether or not, fly is even a method in which Duck should have by default.

Joe realizes that fly can be some sort of interface method which can be implemented. While he is working on that, he also realizes that quack can also be in some interface. Joe finally comes up with an interface to house the actions:

interface FlyBehavior{
    fly();
}

interface QuackBehavior {
    quack();
}

Now, Joe can begin separating away behaviors from the Duck class which are likely to vary. What is interesting about this model is that now he can create different implementations for these specific behaviors. For example:

class FlyWithWings implements FlyBehavior {
    public fly() { ... }
}

class FlyNoWay implements FlyBehavior {
    public fly() { ... }
}

He can do something similar with QuackBehavior:

class Quack implements QuackBehavior {
    public quack() {
        // quack
    }
}

class Squeak implements QuackBehavior {
    public quack() {
        // squeak
    }
}

class MuteQuack implements QuackBehavior {
    public quack() {
        // don't quack
    }
}

Joe now begins to integrate all this behavior back into the Duck class. The way he thinks about it now is that Duck will delegate its flying and quacking behavior to the concrete ducks instead of using the methods directly defined in the class, or any other child classes.

Joe can leverage two new instance variables within Duck called flyBehavior, and quackBehavior to store specific implementations for flying, and quacking.

The fly and quack methods in Duck can then be replaced by performFly, and performQuack to actually invoke the correct behavior stored by a child class.

class Duck {
    QuackBehavior quackBehavior;
    FlyBehavior flyBehavior;
    
    public performQuack() {
        quackBehavior.quack();
    }
    
    public performFly() {
        flyBehavior.fly();
    }
    
    public swim() { ... }
    public display() { ... }
}

Now, let’s see how a child class performs with the change:

class MallardDuck extends Duck {
    public MallardDuck() {
        quackBehavior = new Quack();
        flyBehavior = new FlyWithWings();
    }
    
    ...
}

class RubberDuck extends Duck {
    public RubberDuck() {
        quackBehavior = new Squeak();
        flyBehavior = new FlyNoWay();
    }
    
    ...
}

Joe has now made it so that all child classes are tasked with providing their own way of quacking, and flying. This leaves it up to Mary and Charlie to define how their ducks will quack and fly. This type of design can also allow setting behavior of quacking, and flying dynamically at runtime too!

class Duck {
    QuackBehavior quackBehavior;
    FlyBehavior flyBehavior;
    
    public performQuack() {
        quackBehavior.quack();
    }
    
    public performFly() {
        flyBehavior.fly();
    }
    
    public setQuackBehavior(QuackBehavior qb) {
        quackBehavior = qb;
    }
    
    public setFlyBehavior(FlyBehavior fb) {
        flyBehavior = fb;
    }
    
    public swim() { ... }
    public display() { ... }
}

This is a great example in showing that one should favor composition over inheritance. We can try to make classes HAS-A other things. It can be useful in that:

  1. We allow child classes to provide their own way of doing things.
  2. Allow the outside world to control behavior at run time.

References

  • Head First Design Patterns - Eric Freeman & Elisabeth Freeman - https://www.oreilly.com/library/view/head-first-design/0596007124/