Objects can often have variant algorithms. The usual textbook
example is an object that has two choices for an algorithm, one of which
is slow, but uses little memory, and the other is fast, but requires a
lot of storage for all that speed. In our examples, we can use the
Strategy
pattern to isolate the details
of a betting strategy from the rest of a casino game simulation. This
will allow us to freely add new betting strategies without disrupting
the simulation.
One strategy in Roulette is to always bet on black. Another
strategy is to wait, counting red spins and bet on black after we've
seen six or more reds in a row. These are two alternate player
strategies. We can separate these betting decision algorithms from other
features of player.
We don't want to create an entire subclass of player to reflect
this choice of algorithms. The Strategy design pattern helps us break
something rather complex, like a Player, into separate pieces. The
essential features are in one object, and the algorithm(s) that might
change are in separate strategy object(s). The essential features are
defined in the core class, the other features are strategies that are
used by the core class. We can then create many alternate algorithms as
subclasses of the plug-in Strategy class. At run time, we decide which
strategy object to plug into the core object.
The Two Approaches. As mentioned in the section called “Design Approaches”, we
have two approaches for extending an existing class: wrapping and
inheritance. From an overall view of the collection of classes, the
Strategy design emphasizes wrapping. Our core class is a kind of
wrapper around the plug-in strategy object. The strategy alternatives,
however, usually form a proper class hierarchy and are all
polymorphic.
Let's look at a contrived, but simple example. We have two variant
algorithms for simulating the roll of two dice. One is quick and dirty
and the other more flexible, but slower.
First, we create the basic Dice
class,
leaving out the details of the algorithm. Another object, the strategy
object, will hold the algorithm
class Dice( object ):
def __init__( self, strategy ):
self.strategy= strategy
self.lastRoll= None
def roll( self ):
self.lastRoll= self.strategy.roll()
return self.lastRoll
def total( self ):
return reduce( lambda a,b:a+b, self.lastRoll, 0 )
The Dice
class rolls the dice, and saves
the roll in an instance variable, lastRoll
, so that a
client object can examine the last roll. The total
method computes the total rolled on the dice, irrespective of the actual
strategy used.
The Strategy Class Hierarchy. When an instance of the Dice
class is
created, it must be given a strategy object to which we have delegated
the detailed algorithm. A strategy object must have the expected
interface. The easiest way to be sure it has the proper interface is
to make each alternative a subclass of a strategy superclass.
import random
class DiceStrategy( object ):
def roll( self ):
raise NotImplementedError
The DiceStrategy
class is the superclass
for all dice strategies. It shows the basic method function that all
subclasses must override. We'll define two subclasses that provide
alternate strategies for rolling dice.
The first, DiceStrategy1
is simple.
class DiceStrategy1( DiceStrategy ):
def roll( self ):
return ( random.randrange(6)+1, random.randrange(6)+1 )
This DiceStrategy1
class simply uses the
random
module to create a tuple of two numbers in
the proper range and with the proper distribution.
The second alternate strategy,
DiceStrategy2
, is quite complex.
class DiceStrategy2( DiceStrategy ):
class Die:
def __init__( self, sides=6 ):
self.sides= sides
def roll( self ):
return random.randrange(self.sides)+1
def __init__( self, set=2, faces=6 ):
self.dice = tuple( DiceStrategy2.Die(faces) for d in range(set) )
def roll( self ):
return tuple( x.roll() for x in self.dice )
This DiceStrategy2
class has an internal
class definition, Die
that simulates a single die
with an arbitrary number of faces. An instance variable,
sides
shows the number of sides for the die; the
default number of sides is six. The roll
method
returns are random number in the correct range.
The DiceStrategy2
class creates a number of
instances of Die
objects in the instance variable
dice
. The default is to create two instances of
Die
objects that have six faces, giving us the
standard set of dice for craps. The roll
function
creates a tuple by applying a roll
function to each
of the Die
objects in
self.dice
.
Creating Dice with a Plug-In Strategy. We can now create a set of dice with either of these
strategies.
dice1= Dice( DiceStrategy1() )
dice2 = Dice( DiceStrategy2() )
The dice1
instance of Dice uses an instance of
the DiceStrategy1
class. This strategy object is
used to constuct the instance of Dice
. The
dice2
variable is created in a similar manner, using
an instance of the DiceStrategy2
class.
Both dice1
and dice2
are of
the same class, Dice
, but use different
algorithms to achieve their results. This technique gives us tremendous
flexibility in designing a program.
Multiple Patterns. Construction of objects using the strategy pattern works well
with a
Factory Method
pattern,
touched on in the section called “Factory Method”. We could,
for instance, use a Factory Method to decode input parameters or
command-line options. This give us something like the
following.
class MakeDice( object ):
def newDice( self, strategyChoice ):
if strategyChoice == 1:
strat= DiceStrategy1()
else:
strat= DiceStrategy2()
return Dice( strat )
This allows a program to create the Dice with something like the
following.
dice = MakeDice().newDice(
someInputOption
)
When we add new strategies, we can also subclass the
MakeDice
class to include those new strategy
alternatives.