Home Design Patterns Strategy

Strategy

Behavioral Gang of Four

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

Gamma, Helm, Johnson & Vlissides, p. 315

Purpose

Strategy takes a decision that would otherwise be written as a conditional and turns it into a value. Instead of a method that asks which rule applies and then applies it, you get a method that applies the rule it was handed, and a separate class for each rule.

The gain is that the class doing the work no longer has any opinion about the alternatives. It cannot be broken by a new one, it does not grow when one is added, and it can be tested against a rule that exists only in the test. The class holding the strategy has one job again, and the rules have somewhere to live that is not inside it.

Design

A Context holds a reference to a Strategy interface and calls it. Concrete strategies implement the interface. Someone outside, usually the code that constructs the Context, decides which one it gets.

UML class diagram: Context holds a Strategy interface implemented by three concrete strategies, with a Client that supplies the Context.
Note what is missing compared with the State diagram: no arrow runs back from a strategy to the Context. Strategies do not know each other and do not replace themselves.

The awkward part of the design is always the interface signature. A strategy that needs to see the Context's data has three options, and none of them is clean: take everything as parameters, which grows the signature every time a strategy needs something new; take a reference to the Context, which couples the strategies back to it; or take a small parameter object, which is usually right and is also a fourth class nobody asked for. If you find yourself passing the Context itself, check whether what you actually have is a Template Method.

Example

The Autobot Robotics task scheduler decides which pending job goes to the arm next, and customers do not agree on the rule. A laboratory cell wants the shortest job first, so the researcher waiting on a two-minute run is not stuck behind an hour of production. A regulated line wants strict arrival order, because the audit trail has to show that nothing jumped the queue. A plant where fitting a different end effector takes longer than most of the jobs wants whatever ordering minimizes tool changes.

All three rules had ended up inside nextJob() as a ladder of conditionals over three configuration flags. Two of the flags interacted, and one combination fell through the ladder and returned the pending list untouched, which looked exactly like arrival order and so went unnoticed on the site that had asked for shortest-first. That is the kind of bug that only shows up as "the scheduler seems a bit slow lately".

Ordering is now a JobOrdering object supplied at start-up from the site configuration. The scheduler does not know how many orderings exist. The unexpected benefit was in testing: an ordering is a function from a list of jobs to a list of jobs, so each one gets a unit test with no scheduler, no arm and no clock involved. Before the change, testing the ordering meant standing up the whole scheduler and inferring the rule from what came out of it.

interface JobOrdering
{
    Job[] order(Job[] pending);
}

class TaskScheduler
{
    private JobOrdering ordering;

    public TaskScheduler(JobOrdering o) { ordering = o; }

    // No conditional here, and none will ever be added.
    public Job next(Job[] pending)
    {
        Job[] sorted = ordering.order(pending);
        return sorted.Length == 0 ? null : sorted[0];
    }
}

class ArrivalOrder : JobOrdering
{
    public Job[] order(Job[] pending)
    {
        // sort by queued timestamp, oldest first
        return pending;
    }
}

// Sorts by estimated cycle time, shortest first.
class ShortestFirst : JobOrdering { }

// Groups jobs by the tool they need so the arm swaps end effectors
// as rarely as possible.
class FewestToolChanges : JobOrdering { }

class Site
{
    public TaskScheduler build(Config cfg)
    {
        return new TaskScheduler(cfg.orderingNamed(cfg.schedulerRule));
    }
}
The pattern is the field and the constructor parameter. Everything the scheduler knows about ordering is the one line that calls ordering.order(), and the choice is made in Site.build, which is the only place that has any business making it.

Seen in the Wild

Java's Comparator, handed to Collections.sort or List.sort, is the textbook example, and .NET's IComparer<T> is the same thing under another name. Swing's LayoutManager is a Strategy for arranging child components: a container delegates the entire layout algorithm to an object it is given. java.util.concurrent.ThreadPoolExecutor takes a RejectedExecutionHandler, with AbortPolicy, CallerRunsPolicy and DiscardPolicy supplied as alternatives, which is a Strategy for a policy decision rather than a calculation. Further back, the comparison function pointer passed to C's qsort is the same design without any objects at all, which is a useful reminder that the pattern predates the vocabulary.

Comparisons

  • State is drawn identically and is a different pattern. A strategy is chosen from outside and normally never changes; a state chooses its own successor and changes constantly. Strategies are interchangeable answers to one question; states are positions in a graph and are not interchangeable at all. The tell is whether the objects in the hierarchy know about one another. Strategies do not.
  • Template Method solves the same problem with inheritance rather than composition, and the pair is the clearest illustration of "favor composition over inheritance" in the whole catalog. Template Method varies a step by subclassing and binds the choice at compile time; Strategy varies it by holding an object and binds the choice at run time. Strategy costs an extra object and an extra interface, and buys the ability to change the algorithm on a live instance, to vary two things independently, and to reuse an algorithm outside the hierarchy it was written for.
  • Bridge looks nearly the same in UML. The difference is scale and intent: a Bridge separates an entire implementation layer and expects both hierarchies to keep growing, while a Strategy swaps one algorithm behind one interface.
  • Decorator changes an object's skin, Strategy changes its guts. Reach for Decorator when you cannot modify the class, and Strategy when you can.
  • Dependency Injection is how the strategy usually arrives. The pattern says the Context should be handed its algorithm and says nothing about by whom; that question is where DI starts.
  • Null Object is a strategy that does nothing, and it is the cleanest way to express "no special handling" without a null check in the Context.

Why It Exists

My take

Strategy exists because conditionals accumulate in the wrong place. The scheduler in the example did not set out to know about three ordering rules; it acquired them one reasonable request at a time, and each request was a two-line change that nobody would refuse. The result is a class whose reason to change is now "any customer wants any ordering", which is not a reason to change, it is a category of reasons. Strategy is the move that gives those reasons their own file.

The part I would emphasize is testability, because it is the benefit people undersell. A conditional inside a method can only be tested through that method, which means through everything that method needs to run. Pulling the branch out into an object does not just decouple it, it makes it reachable. The orderings in that example went from being testable only with a live scheduler to being three pure functions with a list going in and a list coming out. That change in testability is usually worth more than the flexibility everyone talks about, and it arrives whether or not anybody ever swaps the strategy at run time.

My position on the pattern is that it is the one most thoroughly absorbed by the languages. A strategy with one method is a function, and in any language with first-class functions, writing an interface plus three classes plus a factory to select between them is ceremony around a parameter. C# delegates, Java lambdas against a functional interface, a plain function value in almost anything modern: that is Strategy, fully implemented, in the language. The reason Comparator survives as a named type is not that it needs a class, it is that the name is worth having and the type can carry extra methods like reversed(). Write the interface when the name earns its keep. Otherwise pass the function and stop.

Criticisms

The client has to know the strategies exist in order to choose one, which means the knowledge you removed from the Context did not vanish, it moved. That is usually an improvement, because it moved to composition-root code that is allowed to know about everything, but it is not the elimination of coupling that the pattern is sometimes sold as. If the choice ends up being made by a conditional in the factory, you have relocated the ladder rather than deleted it.

The interface is the hard part and it tends to erode. Every new strategy that needs one more piece of information either widens the signature for everybody or pushes you toward handing over the Context, and once a strategy holds a reference to its Context the two are coupled again and the hierarchy is no longer reusable. Watch for a strategy interface whose parameter list has grown twice; it means the abstraction was drawn in the wrong place.

It is also the pattern most often applied on speculation. An interface with a single implementation, created because a second one might appear, is pure cost: an extra file, an extra indirection, and a jump-to-definition that lands on an interface instead of on the code. In a hot loop it is a real cost as well, because the call cannot be inlined the way a direct one can. I would rather see the conditional and extract it on the day the second rule actually arrives.