Strategy Design Pattern – Head First Approach

Strategy Design Pattern – Head First Approach

Unlocking Flexibility in Software Design with the Strategy Pattern

The Strategy Pattern is one of the most popular design patterns because it allows you to define a family of algorithms, encapsulate them in different classes, and make them interchangeable. According to the book Head First Design Patterns, the strategy pattern allows a program to select an algorithm at runtime, making it easier to change the behavior of classes without altering their structure.

Let’s explore this pattern step-by-step, just like it is presented in the Head First book, including practical examples with code.


What is the Strategy Pattern?

Definition: The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern lets the algorithm vary independently from the clients that use it.

In simpler terms, the strategy pattern lets you choose different algorithms dynamically without changing the class that uses them. This provides flexibility, especially when you want to offer different behaviors in certain parts of your application.


Problem Scenario

Imagine you're building a game with a variety of characters like ducks. Each duck has common behaviors such as flying and quacking. Initially, you might be tempted to model this by creating a Duck superclass and adding the fly() and quack() methods. However, not all ducks fly or quack the same way.

For example:

  • A rubber duck doesn’t fly or quack.

  • A real duck flies and quacks.

If we hard-code the fly() and quack() methods into the Duck class, we’ll end up with problems when different ducks have different behaviors.


Using the Strategy Pattern

The solution is to encapsulate the behaviors (fly and quack) into their own classes and then use the strategy pattern to dynamically change the behavior at runtime. This way, you can have a flying behavior for real ducks and another for ducks that don’t fly.

Step 1: Encapsulate the behavior

We start by creating two interfaces: FlyBehavior and QuackBehavior, which represent the behaviors of flying and quacking.

public interface FlyBehavior {
    void fly();
}

public interface QuackBehavior {
    void quack();
}

Step 2: Implement concrete behaviors

Next, we create different classes that implement these interfaces. For example, one for ducks that can fly and another for ducks that can’t.

// A real duck's flying behavior
public class FlyWithWings implements FlyBehavior {
    public void fly() {
        System.out.println("I’m flying!!");
    }
}

// A rubber duck's flying behavior (it can't fly)
public class FlyNoWay implements FlyBehavior {
    public void fly() {
        System.out.println("I can’t fly");
    }
}

Similarly, we create quack behaviors:

// A real duck's quacking behavior
public class Quack implements QuackBehavior {
    public void quack() {
        System.out.println("Quack");
    }
}

// A rubber duck's squeaking behavior
public class Squeak implements QuackBehavior {
    public void quack() {
        System.out.println("Squeak");
    }
}

// A mute behavior for ducks that can’t quack
public class MuteQuack implements QuackBehavior {
    public void quack() {
        System.out.println("<< Silence >>");
    }
}

Step 3: Integrate behaviors with the Duck class

Now we integrate these behaviors with the Duck class. Instead of hard-coding the behavior, we use composition to include instances of FlyBehavior and QuackBehavior.

public abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;

    public Duck() {}

    public void performFly() {
        flyBehavior.fly();
    }

    public void performQuack() {
        quackBehavior.quack();
    }

    public void swim() {
        System.out.println("All ducks float, even decoys!");
    }

    public void setFlyBehavior(FlyBehavior fb) {
        flyBehavior = fb;
    }

    public void setQuackBehavior(QuackBehavior qb) {
        quackBehavior = qb;
    }

    public abstract void display();
}

Step 4: Create different types of ducks

We can now create specific types of ducks like MallardDuck and RubberDuck, and assign appropriate behaviors to them.

public class MallardDuck extends Duck {

    public MallardDuck() {
        flyBehavior = new FlyWithWings();
        quackBehavior = new Quack();
    }

    public void display() {
        System.out.println("I’m a real Mallard duck");
    }
}

public class RubberDuck extends Duck {

    public RubberDuck() {
        flyBehavior = new FlyNoWay();
        quackBehavior = new Squeak();
    }

    public void display() {
        System.out.println("I’m a rubber duck");
    }
}

Step 5: Dynamically change behavior

One of the advantages of the strategy pattern is that we can dynamically change a duck’s behavior at runtime. For example, let’s say you want to change a duck’s flying behavior:

public class MiniDuckSimulator {
    public static void main(String[] args) {
        Duck mallard = new MallardDuck();
        mallard.performQuack();
        mallard.performFly();

        Duck rubberDuck = new RubberDuck();
        rubberDuck.performQuack();
        rubberDuck.performFly();

        // Dynamically changing behavior
        rubberDuck.setFlyBehavior(new FlyWithWings());
        rubberDuck.performFly();
    }
}

Output:

Quack
I’m flying!!
Squeak
I can’t fly
I’m flying!!

Bullet Points

  • Encapsulation: The strategy pattern encapsulates behaviors into separate classes, allowing them to be used interchangeably.

  • Flexibility: Behavior can be changed at runtime by simply setting a new strategy.

  • Composition over inheritance: Instead of using inheritance to model different behaviors, composition is used. This leads to more flexible and maintainable code.


When to Use the Strategy Pattern

  • When you need to define a family of algorithms (e.g., different behaviors for ducks).

  • When you want to make an algorithm interchangeable at runtime.

  • When you want to avoid conditional statements to choose between behaviors (like if-else chains).


Conclusion

The strategy pattern provides a flexible way to define families of algorithms and lets you interchange them without modifying the client code. In the case of the duck simulator, it allows us to dynamically change the way ducks behave without altering the core Duck class. This approach, outlined in Head First Design Patterns, emphasizes clean, maintainable, and adaptable code that can evolve as the requirements change.


By using the strategy pattern, you’re not only embracing flexibility and composition, but you’re also following the principles of object-oriented design. This pattern ensures your code remains maintainable and easy to extend in the future.