Objects have state changes. Often the processing that an object
performs depends on the state. In non-object-oriented programming
languages, this state-specific processing is accomplished with long, and
sometimes complex series of
if
statements. The
State
design pattern gives us an
alternative design.
As an example, the game of Craps has two states. A player's first
dice roll is called a come out roll. Depending on
the number rolled, the player immediately wins, immediately loses, or
the game transitions to a point roll. The game
stays in the point roll state until the player makes their point or crap
out with a seven. The following table provides a complete picture of the
state changes and the dice rolls that cause those changes.
Table 23.1. Craps Game States
State |
Roll |
Bet Resolution |
Next State |
Point Off; the Come Out Roll; only Pass and Don't
Pass bets allowed. |
2, 3, 12 |
"Craps": Pass bets lose, Don't Pass bets win. |
Point Off |
7, 11 |
"Winner": Pass bets win, Don't Pass bets lose. |
Point Off |
4, 5, 6, 8, 9, 10 |
No Resolution |
Point On the number rolled,
p
. |
Point On; any additional bets may be placed. |
2, 3, 12 |
No Resolution |
Point still on |
11 |
No Resolution |
Point still on |
7 |
"Loser": all bets lose. The table is cleared. |
Point Off |
Point,
p
|
"Winner": point is made, Pass bets win, Don't Pass bets
lose. |
Point Off |
Non-
p
number |
Nothing; Come bets are activated |
Point still on |
The
State
design pattern is
essential to almost all kinds of programs. The root cause of the hideous
complexity that characterizes many programs is the failure to properly
use the
State
design pattern.
The Craps
Class. The overall game of craps can be represented in an object of
class Craps
. A Craps
object would have a play1Round
function to
initialize the game in the come out roll state, roll dice, pay off
bets, and possibly change states.
Following the
State
design
pattern, we will delegate state-specific processing to an object that
represents just attributes and behaviors unique to each state of the
game. We pan to create a CrapsState
class with
two subclasses: CrapsStateComeOutRoll
and
CrapsStatePointRoll
.
The overall Craps
object will pass the dice
roll to the CrapsState
object for evaluation. The
CrapsState
object calls methods in the original
Craps
object to pay or collect when there is a
win or loss. The CrapsState
object can also
return an object for the next state. Additionally, the
CrapsState
object will have to indicate then the
game actually ends.
We'll look at the Craps
object to see the
context in which the various subclasses of
CrapsState
must operate.
Example 23.1. craps.py
import dice
class Craps( object ):
"""Simple game of craps."""
def __init__( self ):
self.state= None
self.dice= dice.Dice()
self.playing= False
def play1Round( self ):
"""Play one round of craps until win or lose."""
self.state= CrapsStateComeOutRoll()
self.playing= True
while self.playing:
self.dice.roll()
self.state= self.state.evaluate( self, self.dice )
def win( self ):
"""Used by CrapsState when the roll was a winner."""
print "winner"
self.playing= False
def lose( self ):
"""Used by CrapsState when the roll was a loser."""
print "loser"
self.playing= False
|
The Craps class constructor,
__init__ , creates three instance variables:
state , dice and
playing . The state
variable will contain an instance of
CrapsState , either a
CrapsStateComeOutRoll or a
CrapsStatePointRoll . The
dice variable contains an instance of the
class Dice , defined in the section called “Class Definition: the
class
Statement”. The playing
variable is a simple switch that is True
while we the game is playing and False when
the game is over.
|
|
The play1Round method sets the
state to
CrapsStateComeOutRoll , and sets the
playing variable to indicate that the game is
in progress. The basic loop is to roll the dice and the evaluate
the dice.
This method calls the state-specific
evaluate function of the current
CrapsState object. We give this method a
reference to overall game, via the Craps
object. That reference allows the
CrapsState to call the
win or lose method in
the Craps object. The
evaluate function of
CrapsState is also given the
Dice object, so it can get the number
rolled from the dice. Some propositions (called
“hardways”) require that both dice be equal; for
this reason we pass the actual dice to
evaluate , not just the total.
|
|
When the win or
lose method is called, the game ends. These
methods can be called by the the evaluate
function of the current CrapsState . The
playing variable is set to
False so that the game's loop will
end.
|
The CrapsState
Class Hierarchy. Each subclass of CrapsState
has a
different version of the evaluate
operation. Each
version embodies one specific set of rules. This generally leads to a
nice simplification of those rules; the rules can be stripped down to
simple
if
statements that evaluate the dice in one
state only. No additional
if
statements are
required to determine what state the game is in.
class CrapsState( object ):
"""Superclass for states of a craps game."""
def evaluate( self, crapsGame, dice ):
raise NotImplementedError
def __str__( self ):
return self.__doc__
The CrapsState
superclass defines any
features that are common to all the states. One common feature is the
definition of the evaluate
method. The body of the
method is uniquely defined by each subclass. We provide a definition
here as a formal place-holder for each subclass to override. In Java, we
would declare the class and this function as abstract. Python lacks this
formalism, but it is still good practice to include a
placeholder.
Subclasses for Each State. The following two classes define the unique evaluation rules for
the two game states. These are subclasses of
CrapsState
and inherit the common operations
from the superclass.
class CrapsStateComeOutRoll ( CrapsState ):
"""Come out roll rules."""
def evaluate( self, crapsGame, dice ):
if dice.total() in [ 7, 11 ]:
crapsGame.win()
return self
elif dice.total() in [ 2, 3, 12 ]:
crapsGame.lose()
return self
return CrapsStatePointRoll( dice.total() )
The CrapsStateComeOutRoll
provides an
evaluate
function that defines the come out roll
rules. If the roll is an immediate win (7 or 11), it calls back to the
Craps
object to use the win
method. If the roll is an immediate loss (2, 3 or 12), it calls back to
the Craps
object to use the
lose
method. In all cases, it returns an object
which is the next state; this might be the same instance of
CrapsStateComeOutRoll
or a new instance of
CrapsStatePointRoll
.
class CrapsStatePointRoll ( CrapsState ):
"""Point roll rules."""
def __init__( self, point ):
self.point= point
def evaluate( self, crapsGame, dice ):
if dice.total() == 7:
crapsGame.lose()
return None
if dice.total() == self.point:
crapsGame.win()
return None
return self
The CrapsStatePointRoll
provides an
evaluate
function that defines the point roll
rules. If a seven was rolled, the game is a loss, and this method calls
back to the Craps
object to use the
lose
method, which end the game. If the point was
rolled, the game is a winner, and this method calls back to the
Craps
object to use the win
method. In all cases, it returns an object which is the next state. This
might be the same instance of CrapsStatePointRoll
or a new instance of
CrapsStateComeOutRoll
.
Extending the State Design. While the game of craps doesn't have any more states, we can see
how additional states are added. First, a new state subclass is
defined. Then, the main object class and the other states are updated
to use the new state.
An additional feature of the state pattern is its ability to
handle state-specific conditions as well as state-specific processing.
Continuing the example of craps, the only bets allowed on the come out
roll are pass and don't pass bets. All other bets are allowed on the
point rolls.
We can implement this state-specific condition by adding a
validBet
method to the Craps
class. This will return True
if the bet is valid for
the given game state. It will return False
if the bet
is not valid. Since this is a state-specific condition, the actual
processing must be delegated to the CrapsState
subclasses.