The Strategy Pattern
Strategy Pattern
Contextual Forces
Motivation
A single behavior with varying implementation exists, and we want to decouple consumers of this behavior from any particular implementation. We may also want to decouple them from the fact that the implementation is varying at all.
Encapsulation
The various versions of the behavior, how many variations there are, which variation will be used under a given circumstance, and the design of each varying implementation are all hidden from the client. We may also encapsulate the fact that the behavior is varying at all, depending on implementation.
Procedural Analog
A simple switch or other branching logic:, where the condition that drives the branch is a concern of the client or system, but not the algorithm itself (see Chain of Responsibility).
if (condition) {
//Algorithm 1
} else {
//Algorithm 2
}
Non-Software Analog
When I was a young boy, I used to have a Mattel Superstar airplane that had a little plastic disk inside. The grooves and notches on the disk made the airplane fly in a given pattern.
By changing the disk the plane held, I'd get a different flight path. Each disk had a different shape, and as it was rotated on a cam, a feeler rod was moved back and forth turning the rudder as the plane flew.
I didn't need to change anything about the plane to get a different flight path; I just gave it a differently-shaped disk.
All the disks fit in the plane because they were under the maximum size, and had a particular little diamond-shaped hole in their hub that allowed them to mount on the rotating cam. This was the disk's interface, which the plane was designed to accept. So long as the disk conformed to this, I could make new disks that didn't even exist when the plane was created, and they would work, creating new flight behaviors. Only one disk could be mounted at a time, of course.
In this analogy, the Plane is the Context object, and the disks are implementation of the Strategy Object. The size of the disks and the shape/size of the hole at their center is the interface of the Strategy.
Implementation Forces
Example
Strategy, Client instantiates the Strategy
Pseudo Code
public class Context {
public void request(Strategy s) {
s.operation();
}
}
public abstract class Strategy {
public abstract operation();
}
public class Strategy_V1 extends Strategy {
public void operation() {
//Implementation V1
}
}
public class Strategy_V2 extends Strategy {
public void operation() {
//Implementation V2
}
}
Questions, concerns, credibility checks
- Can all the versions of the algorithm (Strategy) really share the same interface? What are the differences between them, and how can I resolve them in such a way that they all can be used in the same way?
- How will the Context object obtain an instance of a Strategy implementation to delegate to?
- How often and under what circumstances will the particular Strategy implementation the client is using change?
- Each time the request is called?
- Each time the Context is instantiated?
- Each time the application is loaded?
- Each time the application is installed?
Options in implementation
- Will the Client hand in a Strategy implementation each time it interacts with the Context?
- Will an object factory be used?
- If an object factory is used, will the Client or the Context interact with the factory?
- Will there be a factory that builds the Context, and hands the Strategy in via its constructor?
Strategy w/Factory, Client uses the factory
Strategy w/Factory, Context uses the factory
Strategy with Context built by a factory
Building the Context or the Strategy with a factory creates less dynamisms, but also more encapsulation. Another version of the pattern, specific to .Net generics, is also non-dynamic but may have performance benefits. See Strategy with .Net Generics.
Consequent Forces
Testing issues
The Strategy implementations can each be tested on its own, but a Mock object should be used to test the Context object. The Mock will take the place of a Strategy implementation and will allow the test to observe the interaction between the Context and the Strategy abstraction.
Cost-Benefit (gain-loss)
- The Context and the Strategy objects will tend toward strong cohesion.
- The Context object is decoupled from the particular Strategy implementations, and therefore we are free to add new implementations or remove existing implementations without this affecting the Context. The issue is open-closed.
- The algorithm provided by the Strategy object must be publicly available, since it is in a separate object. If it were left as code within the Context object it could be private (encapsulated), but once it is "pulled out" into the Strategy, this is not possible.
- The algorithm cannot directly access the state of the Context object. Any state needed by the algorithm must be "passed in" via parameters and any effect that should propagate back to the Context must be returned. This decouples them and makes testing easier, but it can lead to a very broad interface on the Strategy abstraction, which can reduce its reusability.
- More classes are needed when compared to a simple procedural solution within Context (branching logic).
- The delegation from the context to the Strategy object introduces a small degradation in performance.
Generics Implementation
using System; using System.Collections.Generic; using System.Text; namespace DotNetGenerics { class StrategyClient { public static void Main(string[] s) { string clientState = "A123"; //change the "A" to get a different strategy impl string contextData = "purple monkeys"; Context myContext = ContextFactory.makeContext(clientState, contextData); myContext.request(); } } class ContextFactory { public static Context makeContext(string customerCode, string someData) { if (customerCode.StartsWith("A")) { return new GenericContext<Strategy_Impl1>(someData); } else { return new GenericContext<Strategy_Impl2>(someData); } } } interface Context { void request(); } class GenericContext<T> : Context where T : Strategy, new() { T myStrategy; private string myData; public GenericContext(string data) { myData = data; myStrategy = new T(); } public void request() { // other code myData = myStrategy.operation(myData); // more other code } } interface Strategy { string operation(string stringToProcess); } class Strategy_Impl1 : Strategy { public Strategy_Impl1() { } public string operation(string s) { // Perform Impl1 Algorithm on string return s; } } class Strategy_Impl2 : Strategy { public Strategy_Impl2() { } public string operation(string s) { // Perform Impl2 Algorithm on string return s; } } }