Contents
Strategy
From GoF Define a family of algorithms, encapsulate each one, and
make them interchangeable. Strategy lets the algorithms vary independently from
clients that use it. A Decorator lets you change the skin of an object; a
Strategy lets you change the guts.
If an object has a responsibility which can be fulfilled in
many different ways, the appropriate one being chosen at run-time, Strategy is
one of the ways of structuring the code; Decorator from last week is another.
The difference between them is partly in the usage. Sometimes Decorator is not
easy to use; for example, the Circle from the Decorator article. In actual use in a WinForm
or a WebForm, the base Circle would be a member of the Form's Control
collection. To get the Decorated Circle drawn, the Circle would have to be replaced
in the owning control's .Controls collection
by the final Decorator. This is kludgy. It's far better to implement the
behaviour via Strategy.
Using Strategy, the Circle would be given a Strategy object
which contained the appropriate drawing instructions. When the Circle is asked
to Draw, in its Draw method, it would use that Strategy object to do the work,
passing it data about its centre and radius.
It might look like this:
public class Circle {
public virtual void DrawStrategy (DrawStrategy strategy) {
this.drawStrategy = strategy;
}
public virtual void Draw(Graphics g, Pen p){
this.drawStrategy.Draw(this.Center, this.Radius);
}
}
Who gives the Circle the appropriate Strategy? The same
person who decorated the circle; it is unlikely to be the person who asks the
circle to Draw � but might be, sometimes.
It is not immediately possible to 'strategise' the subject (Circle)
in the way that a Decorator can surround a subject, however the Strategy itself can
be decorated to get the same effect.
Unlike Decorator, the object using the Strategy must be
created to use a Strategy; one can't be bolted on later. However, any number of
new Strategies can be added once an object is set up to use one.
The using object is coupled to the Strategy, but not the
other way round; thus a Strategy can potentially be used by many different
objects. Also unlike a Decorator, an object can decide for itself to use a
Strategy, generating the correct one internally; an object cannot choose to be
decorated or not or by what.
One feature of Strategy is that it needs to be passed
context from its user � the centre and radius in the example above. This
context data is usually not required except while the strategy is actually
being executed: it does not have to hold the context as state. This means that
the same Strategy instance can be re-used by many differing clients. This fits
it very well for the Flyweight pattern. Again, this is very different to
Decorator where the Decorator holds the instance it is decorating for its
useful life.
Decorator and Strategy can be seen as two ways to achieve
the same end, applicable in differing circumstances. However, Strategy has
other areas of applicability also. It can be used to rationalise code where
behaviour is conditional on various factors; rather than have complicated
if/switch constructs containing different ways of doing a task, the if/switch
construct can be used to decide on an individual strategy object to use, and
then use it.See Yonat Sharon's OO-Tips site
for a nice justification of Strategy instead of switch/if (that whole site is
worth reading!).
It may also be possible to break out the decision-making
code for the strategy to use into a Factory class, simplifying the
strategy-user even more. Of course, this is only useful if the strategy is used
in more than one place and the decision rules are the same.
Strategies can be nested. Part of the algorithm that the
initial Strategy uses can be varied via a secondary strategy depending on some
of the context data. This can be repeated indefinitely.
One 'problem' with Strategies is that not all concrete
strategies use all the context passed to them. To keep the interface the same,
the greatest possible context must be passed, but this will be wasted on simple
strategies. One solution is to pass the using object itself; this allows the
strategy to ask for the data it needs directly, but has the disadvantage of
coupling the strategy to one using class.
A solution to this is to have the users implement an
interface that the Strategy expects and can query to get the data required to
implement its algorithm. This is a nice solution, but should be used wisely; it
complicates the code structure and should be used with care.
public class StrategyUser : IMyStrategyForXyzzyInfo {
public void DoSomething() {
IMyStrategyForXyzzyInfo isi = this as IMyStrategyForXyzzyInfo
this.myStrategy.DoSomething(isi);
}
}
Mediator is a 'heavy' strategy,
where more than one class must provide the context for the calculation. Mediators can end up using multiple strategies.
Consequences from GoF:
- Families of related algorithms
- Algorithms
are arranged into an inheritance hierarchy.
- Similarities
between algorithms can be moved into the Strategy base class.
- An
alternative to subclassing
- Achieves
a clearer, more extensible design than subclassing context.
- The
algorithm can be varied independent of the context
- The
algorithm can be changed dynamically at run time.
- Strategies
eliminate conditional statements
- When
the strategies are lumped into one class it is hard to avoid conditionals
- Conditional
statements are hard to maintain, bulky and create unnecessary
dependencies
- A
choice of implementations
- Clients
can chose amongst different implementations with different time and space
trade-offs
- Clients
must be aware of different strategies
- One drawback is that clients must be aware of the effects of different
strategies
- Exposes
clients to implementation issues
- Communication
overhead between Strategy and Context
- Be
wary of passing a lot of data between Context and Strategy in which most
of the concrete Strategy classes do not use this data.
- Use
tighter coupling or an Interface to avoid this situation
- Increased
number of objects
- This
pattern increases the number of objects in an application.
- This
overhead can be reduced by implementing strategies as stateless objects
and maintaining state in the context. The concrete strategy instances can
then use the Flyweight pattern to share instances across multiple users.
A Flyweight implementation of a Strategy:
static void Main(string[] args) {
for (int i = 0; i < 26; i++) {
string type = "ABC".Substring(i % 3, 1);
Strategy strategy = StrategyFactory.Strategy(type);
Context c = new Context(strategy);
c.DoSomething();
}
}
public class Context {
public Context(Strategy concreteStrategy) {
this.concreteStrategy = concreteStrategy;
}
private int someState;
private Strategy concreteStrategy;
public int SomeState {
get { return this.someState; }
set { this.someState = value; }
}
public void DoSomething() {
this.concreteStrategy.DoWhatever(this.someState);
}
}
public abstract class Strategy {
protected int instrinsicState;
public virtual void DoWhatever(int helpfulData) {}
}
public class ConcreteStrategyA : Strategy {
internal ConcreteStrategyA() {
instrinsicState = 12;
}
public override void DoWhatever(int helpfulData) {
// do whatever in a particular way
}
}
public class StrategyFactory {
private static Hashtable strategies = new Hashtable();
public static Strategy Strategy(string strategyType) {
Strategy strategyToReturn = null;
if (!strategies.Contains(strategyType)) {
switch (strategyType) {
case "A" :
strategyToReturn = new ConcreteStrategyA();
break;
// other cases
}
strategies.Add(strategyType, strategyToReturn);
} else {
strategyToReturn =
strategies[strategyType] as Strategy;
}
return strategyToReturn;
}
}