The Strategy Design Pattern

One of the simpler and more useful tools in OOD is the Strategy design pattern. It gives you a lot of flexibility and it’s a great introduction to concepts in software modeling like polymorphism and inversion of control (IOC). The Gang of Four explain the intent of the Strategy pattern:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.

Before we get to some of the OO stuff in the pattern, and why it matters, let’s take a look at the demo we’ll be using. An N-body simulator physically models bodies of varying mass and calculates the forces between them. The fun part is setting up different initial conditions. Even with the same physics rules, you can get an impressive variety of behavior by changing the initial conditions (e.g., the number of bodies, their placement and individual mass). You can also change the global conditions like the gravity constant, damping and the speed of the simulation. Since we want to encapsulate those aspects of the simulator, the Strategy pattern is perfect.

The Strategy can be implemented in pretty much any OO language, but we’re using JavaScript here so we can see the results of the demo in the browser. We’re also using the excellent Paper.js library to handle some of the 2D drawing and a little bit of vector math.

N-body Simulator with five different strategy classes. Click for working demo and source code.

The demo has five different Strategy classes that can be changed using the buttons. As you press the buttons, the simulator plugs in a different Strategy class. In keeping with the definition above, note that each N-Body Strategy is interchangeable and, as a group, can be thought of as a family of algorithms.

In our simulator, it’s important to understand the responsibilities of the N-body Strategy classes. Each class is designed to

  1. Set the number of bodies
  2. Set the location, mass, color, size and (optional) the initial velocity of each body
  3. Accumulate the forces acting on the bodies each cycle
  4. Integrate the positions of the bodies each cycle

In the general example case, a Strategy class has a single method that’s implemented. That method is called execute()—as in execute the algorithm of this Strategy. The design pattern doesn’t require whether the method is called once, or every cycle or even that it’s named execute (it’s always better to name it something a little more descriptive). The pattern only requires the execute() method be named the same in each Strategy and can be called the same way by other classes or clients. The key is that it’s named the same, but implemented differently. This is polymorphism: same name, different behavior.

The Strategy design pattern also has another component, the Context class. The Context acts as a parent to the Strategy classes, keeping a collection of Strategies. The Context handles how Strategy classes are added, activated and used in the application.

Static class UML diagram for the N-body Simulator. Click to view.

In our case, the execute-like method is called simulate(). Each cycle, the forces affecting every simulated body are calculated and their positions are integrated. The simulate() method takes care of responsibilities three and four in the list above. The constructor of each N-Body Strategy class takes care of the first and second. Even though the initial conditions are in the constructor, they still are very much part of each Strategy’s algorithm.

The NBodyContextclass is the Context for the N-body Strategy classes. Let’s take a look at the code for the main application cycle in NBodyContext.

NBodyContext.prototype.run = function () {
    var $this = this;
    view.onFrame = function (event) {
        $this.simStrategy.simulate();
        $this.draw();
    }
}

Notice that the run() method in NBodyContext doesn’t care which Strategy is currently active. This is why polymorphism is so powerful. As long as the interface of each Strategy class remains consistent, we don’t have to do any object testing or use any conditionals to alter the behavior of the system. It just works. 

If you’re interested in OOD you’re probably familiar with the concept favor composition over inheritance. The Strategy pattern is very much compositional. The NBodyContext class is composed of the current Strategy class. An alternative would be to subclass the Context each time you wanted different behavior in the simulation, but the behavior would then be hardwired into the class. You’d lose the flexibility of just changing the behavior and you’d have to carry around all the higher level code—like the main application loop—inherited in each Context subclass.

The use of the Strategy pattern is compositional in nature, but that doesn’t mean a Strategy class can’t take advantage of inheritance. That’s what we’ve done with AbstractStrategy. All Strategy classes inherit fromAbstractStrategy and, by default, they only have to setup the initial conditions of a simulation. Everything else is inherited. As a result, classes like SpiralStrategy are quite simple.

"use strict";

/*
 * A strategy that creates a spiral configuration of bodies, 
 * increasing in mass and size.
 */
var SpiralStrategy = function () {

    AbstractStrategy.call(this, 
            {timeStep:1/20, gravity:0.1, damping:0.999});

    var scale = 20;
    var count = 150;
    var radCoef = 0.3;
    var mssCoef = 0.01;

    var colorA = new Color(1.0, 0.0, 1.0, 0.9);
    var colorB = new Color(1.0, 0.5, 0.0, 0.9);

    var c = view.center.clone();
    c.x -= 200;

    for (var i = 1; i <= count; i++) {

        c.x += Math.sin(i * 0.1) * scale;
        c.y += Math.cos(i * 0.1) * (scale += 1);

        var rad = i * radCoef + 1;
        var mss = i * mssCoef + 1;
        var color = (i % 2 == 0) ? colorA : colorB;

        this.addBody(
                new CircleBody(c.x, c.y, rad, mss, color));
    }
}

SpiralStrategy.prototype = 
        Object.create(AbstractStrategy.prototype);
SpiralStrategy.prototype.constructor = SpiralStrategy;

The default behavior of our Strategy child classes uses the accumulateForces() method inherited from AbstractStrategy. That accumulator doesn’t take into account the distance of the bodies when determining forces.

If we want to change the way a child class behaves, respective to its parent, we can override a method in the child class. That’s what we’re doing in the DistanceForceStrategy class. The overridden accumulateForces() method adds distance to the force equation. Similarly, the MouseEventStrategy class overrides the accumulateForces() to allow the mouse to affect the behavior of the simulation.

There’s a worthwhile distinction between MouseEventStrategy and DistanceForceStrategy. The former overrides the accumulateForces() method and calls that method in the parent abstract class. Then it just adds a little extra code to update the mouse behavior.

MouseEventStrategy.prototype.accumulateForces = function () {
    AbstractStrategy.prototype.accumulateForces.call(this);
    this.mouseBody.position = this.mousePoint;
}

The DistanceForceStrategy class, on the other hand, completely overrides the same method without calling it in the superclass. These are two flexible approaches to subclassing and overriding methods. Overall you should favor composition, but inheritance is very powerful where appropriate. Learning to spot good inheritance vs. bad inheritance is key to becoming a good architect.

So what makes this an example of IOC? Well, it’s a bit of a simple demo, but its behavior is delegated to modular components—the Strategy classes. The reusable, unchanging part of the simulator—the container—makes calls to the pluggable, swappable Strategy classes. Only the Strategy classes need to be added or altered to extend and change the system behavior. For example, you could scrap all the current Strategy classes and write twenty new ones while the overarching system remains the same. Anytime you have a modular, Strategy-based architecture like this, it’s almost always an example of IOC.

Some things to think about:

  1. How would you make the drawing into a Strategy so different 2D libraries could be swapped in and out?
  2. The CircleBody class is responsible for the actual integration algorithm. Is this good design? What if you wanted to change the integrator during runtime? The current one is Verlet integration. What if you wanted to swap it to Euler?
  3. What if you wanted a square shaped body. How would you handle it?
  4. Could you make a Strategy that recorded positions over time? Imagine positions could be replayed without using the integrator and mathematical calculations.

 

1 thought on “The Strategy Design Pattern”

Leave a Reply

Your email address will not be published. Required fields are marked *