Sponsored Link •
|
Advertisement
|
In Guideline 1, I said that objects are machines. The kind of machine that service-oriented objects resemble, mathematically speaking at least, is the state machine. A state machine is defined by:
When a state machine is in some way built and put into motion, it starts out its lifetime in its initial state. At any time during its life it has a current state. The outside world interacts with the state machine by sending it messages. When the state machine receives a message, it performs actions, including potentially changing state.
Similarly, when a service-oriented object is instantiated, it starts out its lifetime in some initial state, which is established by the object's constructor. At any time during its life it has a current state. The outside world interacts with the object by invoking its methods. When a method is invoked, the object performs actions, including potentially changing state, and returns.
Most often, the behavior of objects is defined by some human language description, such as a written specification. Now and then, however, you'll see behavior specified with formal state machines. For example, the Java Telephony API specification includes a state-transition diagram that defines the states that a single "call" can progress through.
By sheer coincidence, the behavior of the stamp dispenser example from Guideline 3, the can also be expressed in terms of a state machine. The stamp dispenser state machine has:
HAS_0
, HAS_5
, HAS_10
,
HAS_15
add5
, add10
, returnCoins
dispenseStamp
, ret5
,
ret10
, ret15
A stamp dispenser's current state indicates how much money has been inserted. If no money
has been inserted, the stamp dispenser is in the HAS_0
state. If a nickel
has been inserted, the stamp dispenser is in the HAS_5
state, and so on.
No HAS_20
state appears in the list, because as soon as 20 cents has been
inserted, a stamp is automatically issued and any change is returned.
The three messages represent the actions
the user of a stamp dispenser can take: inserting a nickel (add5
),
inserting a dime (add10
), or pressing the coin
return lever (returnCoins
).
The four actions the stamp dispenser
can take are return a nickel (ret5
), return a dime (ret10
), return
15 cents (ret15
), or dispense a 20 cent stamp (dispenseStamp
).
Table 4-1 shows the state-transition table that defines the actions taken and next state that results from a stamp dispenser receiving each message in each state:
Current State | Message | Action | Next State |
---|---|---|---|
HAS_0 | add5 | HAS_5 | |
HAS_0 | add10 | HAS_10 | |
HAS_0 | returnCoins | HAS_0 | |
HAS_5 | add5 | HAS_10 | |
HAS_5 | add10 | HAS_15 | |
HAS_5 | returnCoins | ret5 | HAS_0 |
HAS_10 | add5 | HAS_15 | |
HAS_10 | add10 | dispenseStamp | HAS_0 |
HAS_10 | returnCoins | ret10 | HAS_0 |
HAS_15 | add5 | dispenseStamp | HAS_0 |
HAS_15 | add10 | dispenseStamp,ret5 | HAS_0 |
HAS_15 | returnCoins | ret15 | HAS_0 |
Initial State: HAS_0 |
Figure 4-2 shows the state-transition diagram for the same state machine:
I started out this guideline by saying that service-oriented objects resemble state machines, once those state machines are built and put into motion. The state machine has to be built, not just specified, to act like a service-oriented object. But how do you build a state machine? Well, one way to build a state machine is to model it with a service-oriented object. Given any state machine, you can directly map a state machine to a service-oriented object like this:
For example, the stamp dispenser state machine defined in Table 4-1 can be directly mapped to the object defined by the following class:
Listing 4-1. A code-heavy stamp dispenser.
1 package com.artima.examples.stampdispenser.ex2; 2 3 import java.util.Set; 4 import java.util.Iterator; 5 import java.util.HashSet; 6 7 /** 8 * A stamp dispenser that accepts nickels and dimes and dispenses 9 * twenty cent stamps. 10 * 11 * @author Bill Venners 12 */ 13 public class StampDispenser { 14 15 private static final int HAS_0 = 0; 16 private static final int HAS_5 = 1; 17 private static final int HAS_10 = 2; 18 private static final int HAS_15 = 3; 19 20 private int currentState = HAS_0; 21 private Set listeners = new HashSet(); 22 23 /** 24 * Constructs a new stamp dispenser with a starting balance of zero. 25 */ 26 public StampDispenser() { 27 } 28 29 /** 30 * Adds the specified stamp dispenser listener to receive stamp dispenser events 31 * from this stamp dispenser. If <code>l</code> is <code>null</code>, no exception 32 * is thrown and no action is performed. If <code>l</code> is already registered 33 * as a listener, no action is performed. 34 */ 35 public synchronized void addStampDispenserListener(StampDispenserListener l) { 36 37 listeners.add(l); 38 } 39 40 /** 41 * Removes the specified stamp dispenser listener so that it no longer 42 * receives stamp dispenser events from this stamp dispenser. This method 43 * performs no function, nor does it throw an exception, if the listener 44 * specified by the argument was not previously added to this 45 * component. If <code>l</code> is <code>null</code>, no exception is 46 * thrown and no action is performed. 47 */ 48 public synchronized void removeStampDispenserListener(StampDispenserListener l) { 49 50 listeners.remove(l); 51 } 52 53 /** 54 * Add 5 cents to the stamp dispenser. If the amount added 55 * causes the current value to become or exceed 20 cents, the price of 56 * a stamp, the stamp will be automatically dispensed. 57 */ 58 public synchronized void add5() { 59 60 switch (currentState) { 61 62 case HAS_0: 63 StampDispenserEvent event = new StampDispenserEvent(this, 0, 5); 64 fireCoinAccepted(event, listeners); 65 currentState = HAS_5; 66 break; 67 68 case HAS_5: 69 event = new StampDispenserEvent(this, 0, 10); 70 fireCoinAccepted(event, listeners); 71 currentState = HAS_10; 72 break; 73 74 case HAS_10: 75 event = new StampDispenserEvent(this, 0, 15); 76 fireCoinAccepted(event, listeners); 77 currentState = HAS_15; 78 break; 79 80 case HAS_15: 81 82 event = new StampDispenserEvent(this, 0, 0); 83 fireStampDispensed(event, listeners); 84 currentState = HAS_0; 85 break; 86 } 87 } 88 89 /** 90 * Add 10 cents to the stamp dispenser. If the amount added 91 * causes the current value to become or exceed 20 cents, the price of 92 * a stamp, the stamp will be automatically dispensed. 93 */ 94 public synchronized void add10() { 95 96 switch (currentState) { 97 98 case HAS_0: 99 StampDispenserEvent event = new StampDispenserEvent(this, 0, 10); 100 fireCoinAccepted(event, listeners); 101 currentState = HAS_10; 102 break; 103 104 case HAS_5: 105 event = new StampDispenserEvent(this, 0, 15); 106 fireCoinAccepted(event, listeners); 107 currentState = HAS_15; 108 break; 109 110 case HAS_10: 111 event = new StampDispenserEvent(this, 0, 0); 112 fireStampDispensed(event, listeners); 113 currentState = HAS_0; 114 break; 115 116 case HAS_15: 117 118 event = new StampDispenserEvent(this, 5, 0); 119 fireStampDispensed(event, listeners); 120 currentState = HAS_0; 121 break; 122 } 123 } 124 125 /** 126 * Returns coins. If the balance is zero, no action is 127 * performed. 128 */ 129 public synchronized void returnCoins() { 130 131 switch (currentState) { 132 133 case HAS_0: 134 currentState = HAS_0; 135 break; 136 137 case HAS_5: 138 StampDispenserEvent event = new StampDispenserEvent(this, 5, 0); 139 fireCoinsReturned(event, listeners); 140 currentState = HAS_0; 141 break; 142 143 case HAS_10: 144 event = new StampDispenserEvent(this, 10, 0); 145 fireCoinsReturned(event, listeners); 146 currentState = HAS_0; 147 break; 148 149 case HAS_15: 150 151 event = new StampDispenserEvent(this, 15, 0); 152 fireCoinsReturned(event, listeners); 153 currentState = HAS_0; 154 break; 155 } 156 } 157 158 /** 159 * Helper method that fires coinAccepted events. 160 */ 161 private static void fireCoinAccepted(StampDispenserEvent event, 162 Set listeners) { 163 164 Iterator it = listeners.iterator(); 165 while (it.hasNext()) { 166 StampDispenserListener l = (StampDispenserListener) it.next(); 167 l.coinAccepted(event); 168 } 169 } 170 171 /** 172 * Helper method that fires stampDispensed events. 173 */ 174 private static void fireStampDispensed(StampDispenserEvent event, 175 Set listeners) { 176 177 Iterator it = listeners.iterator(); 178 while (it.hasNext()) { 179 StampDispenserListener l = (StampDispenserListener) it.next(); 180 l.stampDispensed(event); 181 } 182 } 183 184 /** 185 * Helper method that fires coinsReturned events. 186 */ 187 private static void fireCoinsReturned(StampDispenserEvent event, 188 Set listeners) { 189 190 Iterator it = listeners.iterator(); 191 while (it.hasNext()) { 192 StampDispenserListener l = (StampDispenserListener) it.next(); 193 l.coinsReturned(event); 194 } 195 } 196 }
This StampDispenser
really offers the same service to clients as the
StampDispenser
shown in Guideline 3, but
this StampDispenser
has a less intuitive interface
and a more code-heavy implementation. I prefer both the interface and implementation
of the StampDispenser
shown in Guideline 3. I find the single add(int)
method of Guideline 3's StampDispenser
to be easier to understand than
this StampDispenser
's separate add5()
and add10()
methods.
And I prefer the more concise implementations of the add
and
returnCoins
methods in Guideline 3's StampDispenser
to this StampDispenser
's elaborate switch statements.
Nevertheless, this StampDispenser
has value because it serves as
a convenient launching point for a discussion about the state pattern.
The state pattern suggests that when you find yourself with switch statements
with a case for each of some finite number
of states, you should consider factoring out the behavior for each state into
its own class. For example, in the code-heavy StampDispenser
of
Listing 4-1, the add5
, add10
, and
returnCoins
methods are implemented as switch statements. The
switch statements have
one case for each of the stamp dispenser state machine's four states:
HAS_0
, HAS_5
, HAS_10
, and HAS_15
.
Thus, the state pattern suggests refactoring this class by collecting all the code for
each stamp dispenser state into its own class. This refactoring process yields five new classes: an
abstract State
class, and four concrete subclasses, one for each
of the four states of the state machine: Has0State
, Has5State
,
Has10State
, Has15State
.
You start by defining an abstract class (or interface) that contains a method for
each message that can be sent to the state machine. In the stamp dispenser case, the
messages are add5
, add10
, and returnCoins
.
Here's an abstract class named State
that defines three abstract methods, one
for each message:
Listing 4-2. Class StampDispenserState
.
1 package com.artima.examples.stampdispenser.ex3; 2 3 import java.util.Iterator; 4 import java.util.Set; 5 6 /** 7 * Abstract superclass for all stamp dispenser state classes. 8 * 9 * The state machine that embodies the behavior of the 10 * stamp dispenser (which determines the behavior of the 11 * <CODE>StampDispenserState</code> subclasses) is described 12 * in this <a href="../../../../../statemachine.html">state transition table</a>. 13 */ 14 abstract class StampDispenserState { 15 16 /** 17 * Performs actions appropriate for the state represented by 18 * the class of this object for the add5 message. Returns a 19 * reference to the state object that represents the next state 20 * to transition to as a result of the arrival of this add5 message. 21 * 22 * @param listeners <code>Set</code> containing zero to many (and only) 23 * <code>StampDispenserListener</code>s 24 */ 25 abstract StampDispenserState add5(StampDispenser stampDispenser, 26 Set listeners); 27 28 /** 29 * Performs actions appropriate for the state represented by 30 * the class of this object for the add10 message. Returns a 31 * reference to the state object that represents the next state 32 * to transition to as a result of the arrival of this add10 message. 33 * 34 * @param listeners <code>Set</code> containing zero to many (and only) 35 * <code>StampDispenserListener</code>s 36 */ 37 abstract StampDispenserState add10(StampDispenser stampDispenser, 38 Set listeners); 39 40 /** 41 * Performs actions appropriate for the state represented by 42 * the class of this object for the returnCoins message. Returns a 43 * reference to the state object that represents the next state 44 * to transition to as a result of the arrival of this returnCoins message. 45 * 46 * @param listeners <code>Set</code> containing zero to many (and only) 47 * <code>StampDispenserListener</code>s 48 */ 49 abstract StampDispenserState returnCoins(StampDispenser stampDispenser, 50 Set listeners); 51 52 /** 53 * Helper method that fires coinAccepted events. 54 * 55 * @param event <code>StampDispenserEvent</code> to propagate 56 * @param listeners <code>Set</code> containing zero to many (and only) 57 * <code>StampDispenserListener</code>s 58 */ 59 static void fireCoinAccepted(StampDispenserEvent event, 60 Set listeners) { 61 62 Iterator it = listeners.iterator(); 63 while (it.hasNext()) { 64 StampDispenserListener l = (StampDispenserListener) it.next(); 65 l.coinAccepted(event); 66 } 67 } 68 69 /** 70 * Helper method that fires stampDispensed events. 71 * 72 * @param event <code>StampDispenserEvent</code> to propagate 73 * @param listeners <code>Set</code> containing zero to many (and only) 74 * <code>StampDispenserListener</code>s 75 */ 76 static void fireStampDispensed(StampDispenserEvent event, 77 Set listeners) { 78 79 Iterator it = listeners.iterator(); 80 while (it.hasNext()) { 81 StampDispenserListener l = (StampDispenserListener) it.next(); 82 l.stampDispensed(event); 83 } 84 } 85 86 /** 87 * Helper method that fires coinsReturned events. 88 * 89 * @param event <code>StampDispenserEvent</code> to propagate 90 * @param listeners <code>Set</code> containing zero to many (and only) 91 * <code>StampDispenserListener</code>s 92 */ 93 static void fireCoinsReturned(StampDispenserEvent event, 94 Set listeners) { 95 96 Iterator it = listeners.iterator(); 97 while (it.hasNext()) { 98 StampDispenserListener l = (StampDispenserListener) it.next(); 99 l.coinsReturned(event); 100 } 101 } 102 }
You then define one concrete subclass for each state.
In this example, the four concrete State
subclasses -- named
Has0State
, Has5State
, Has10State
, and
Has15State
-- are all implemented as singletons.
The methods of class Has0State
are responsible for fulfilling the stamp dispenser's contract when it is in
the HAS_0
state.
Likewise, the methods of class Has5State
are responsible for fulfilling the
stamp dispenser's contract when it is in
the HAS_5
state.
Here are all four concrete State
subclasses:
Listing 4-3. Class Has0State
.
1 package com.artima.examples.stampdispenser.ex3; 2 3 import java.util.Set; 4 5 /** 6 * <code>StampDispenserState</code> subclass that represents 7 * the <em>Has0</em> stamp dispenser state. See the documentation 8 * for class <code>StampDispenserState</code> for a state 9 * transition table that specifies the required behavior of 10 * instances of this class. 11 */ 12 class Has0State extends StampDispenserState { 13 14 /** 15 * The single instance of <code>Has0State</code> 16 */ 17 private static Has0State singleton = new Has0State(); 18 19 /** 20 * Constructs a <code>Has0State</code> instance. This constructor is 21 * private to enable this class to restrict the number 22 * of instances of <code>Has0State</code> to one. I.e., <code>Has0State</code> is 23 * a singleton. 24 */ 25 private Has0State() { 26 } 27 28 /** 29 * Factory method that returns the single instance of 30 * <code>Has0State</code>. 31 */ 32 static StampDispenserState getState() { 33 return singleton; 34 } 35 36 /** 37 * Performs actions appropriate for the state represented by 38 * the class of this object for the add5 message. Returns a 39 * reference to the state object that represents the next state 40 * to transition to as a result of the arrival of this add5 message. 41 * 42 * @param stampDispenser the <code>StampDispenser</code> whose state this 43 * object represents 44 * @param listeners <code>Set</code> containing zero to many (and only) 45 * <code>StampDispenserListener</code>s 46 */ 47 StampDispenserState add5(StampDispenser stampDispenser, 48 Set listeners) { 49 50 StampDispenserEvent event = new StampDispenserEvent(stampDispenser, 0, 5); 51 fireCoinAccepted(event, listeners); 52 return Has5State.getState(); 53 } 54 55 /** 56 * Performs actions appropriate for the state represented by 57 * the class of this object for the add10 message. Returns a 58 * reference to the state object that represents the next state 59 * to transition to as a result of the arrival of this add10 message. 60 * 61 * @param stampDispenser the <code>StampDispenser</code> whose state this 62 * object represents 63 * @param listeners <code>Set</code> containing zero to many (and only) 64 * <code>StampDispenserListener</code>s 65 */ 66 StampDispenserState add10(StampDispenser stampDispenser, 67 Set listeners) { 68 69 StampDispenserEvent event = new StampDispenserEvent(stampDispenser, 0, 10); 70 fireCoinAccepted(event, listeners); 71 return Has10State.getState(); 72 } 73 74 /** 75 * Performs actions appropriate for the state represented by 76 * the class of this object for the returnCoins message. Returns a 77 * reference to the state object that represents the next state 78 * to transition to as a result of the arrival of this returnCoins message. 79 * 80 * @param stampDispenser the <code>StampDispenser</code> whose state this 81 * object represents 82 * @param listeners <code>Set</code> containing zero to many (and only) 83 * <code>StampDispenserListener</code>s 84 */ 85 StampDispenserState returnCoins(StampDispenser stampDispenser, 86 Set listeners) { 87 88 return this; 89 } 90 }
Listing 4-4. Class Has5State
.
1 package com.artima.examples.stampdispenser.ex3; 2 3 import java.util.Set; 4 5 /** 6 * <code>StampDispenserState</code> subclass that represents 7 * the <em>Has5</em> stamp dispenser state. See the documentation 8 * for class <code>StampDispenserState</code> for a state 9 * transition table that specifies the required behavior of 10 * instances of this class. 11 */ 12 class Has5State extends StampDispenserState { 13 14 /** 15 * The single instance of <code>Has0State</code> 16 */ 17 private static Has5State singleton = new Has5State(); 18 19 /** 20 * Constructs a <code>Has5State</code> instance. This constructor is 21 * private to enable this class to restrict the number 22 * of instances of <code>Has5State</code> to one. I.e., <code>Has5State</code> is 23 * a singleton. 24 */ 25 private Has5State() { 26 } 27 28 /** 29 * Factory method that returns the single instance of 30 * <code>Has5State</code>. 31 */ 32 static StampDispenserState getState() { 33 return singleton; 34 } 35 36 /** 37 * Performs actions appropriate for the state represented by 38 * the class of this object for the add5 message. Returns a 39 * reference to the state object that represents the next state 40 * to transition to as a result of the arrival of this add5 message. 41 * 42 * @param stampDispenser the <code>StampDispenser</code> whose state this 43 * object represents 44 * @param listeners <code>Set</code> containing zero to many (and only) 45 * <code>StampDispenserListener</code>s 46 */ 47 StampDispenserState add5(StampDispenser stampDispenser, 48 Set listeners) { 49 50 StampDispenserEvent event = new StampDispenserEvent(stampDispenser, 0, 10); 51 fireCoinAccepted(event, listeners); 52 return Has10State.getState(); 53 } 54 55 /** 56 * Performs actions appropriate for the state represented by 57 * the class of this object for the add10 message. Returns a 58 * reference to the state object that represents the next state 59 * to transition to as a result of the arrival of this add10 message. 60 * 61 * @param stampDispenser the <code>StampDispenser</code> whose state this 62 * object represents 63 * @param listeners <code>Set</code> containing zero to many (and only) 64 * <code>StampDispenserListener</code>s 65 */ 66 StampDispenserState add10(StampDispenser stampDispenser, 67 Set listeners) { 68 69 StampDispenserEvent event = new StampDispenserEvent(stampDispenser, 0, 15); 70 fireCoinAccepted(event, listeners); 71 return Has15State.getState(); 72 } 73 74 /** 75 * Performs actions appropriate for the state represented by 76 * the class of this object for the returnCoins message. Returns a 77 * reference to the state object that represents the next state 78 * to transition to as a result of the arrival of this returnCoins message. 79 * 80 * @param stampDispenser the <code>StampDispenser</code> whose state this 81 * object represents 82 * @param listeners <code>Set</code> containing zero to many (and only) 83 * <code>StampDispenserListener</code>s 84 */ 85 StampDispenserState returnCoins(StampDispenser stampDispenser, 86 Set listeners) { 87 88 StampDispenserEvent event = new StampDispenserEvent(stampDispenser, 5, 0); 89 fireCoinsReturned(event, listeners); 90 return Has0State.getState(); 91 } 92 }
Listing 4-5. Class Has10State
.
1 package com.artima.examples.stampdispenser.ex3; 2 3 import java.util.Set; 4 5 /** 6 * <code>StampDispenserState</code> subclass that represents 7 * the <em>Has10</em> stamp dispenser state. See the documentation 8 * for class <code>StampDispenserState</code> for a state 9 * transition table that specifies the required behavior of 10 * instances of this class. 11 */ 12 class Has10State extends StampDispenserState { 13 14 /** 15 * The single instance of <code>Has0State</code> 16 */ 17 private static Has10State singleton = new Has10State(); 18 19 /** 20 * Constructs a <code>Has10State</code> instance. This constructor is 21 * private to enable this class to restrict the number 22 * of instances of <code>Has10State</code> to one. I.e., <code>Has10State</code> is 23 * a singleton. 24 */ 25 private Has10State() { 26 } 27 28 /** 29 * Factory method that returns the single instance of 30 * <code>Has10State</code>. 31 */ 32 static StampDispenserState getState() { 33 return singleton; 34 } 35 36 /** 37 * Performs actions appropriate for the state represented by 38 * the class of this object for the add5 message. Returns a 39 * reference to the state object that represents the next state 40 * to transition to as a result of the arrival of this add5 message. 41 * 42 * @param stampDispenser the <code>StampDispenser</code> whose state this 43 * object represents 44 * @param listeners <code>Set</code> containing zero to many (and only) 45 * <code>StampDispenserListener</code>s 46 */ 47 StampDispenserState add5(StampDispenser stampDispenser, 48 Set listeners) { 49 50 StampDispenserEvent event = new StampDispenserEvent(stampDispenser, 0, 15); 51 fireCoinAccepted(event, listeners); 52 return Has15State.getState(); 53 } 54 55 /** 56 * Performs actions appropriate for the state represented by 57 * the class of this object for the add10 message. Returns a 58 * reference to the state object that represents the next state 59 * to transition to as a result of the arrival of this add10 message. 60 * 61 * @param stampDispenser the <code>StampDispenser</code> whose state this 62 * object represents 63 * @param listeners <code>Set</code> containing zero to many (and only) 64 * <code>StampDispenserListener</code>s 65 */ 66 StampDispenserState add10(StampDispenser stampDispenser, 67 Set listeners) { 68 69 StampDispenserEvent event = new StampDispenserEvent(stampDispenser, 0, 0); 70 fireStampDispensed(event, listeners); 71 return Has0State.getState(); 72 } 73 74 /** 75 * Performs actions appropriate for the state represented by 76 * the class of this object for the returnCoins message. Returns a 77 * reference to the state object that represents the next state 78 * to transition to as a result of the arrival of this returnCoins message. 79 * 80 * @param stampDispenser the <code>StampDispenser</code> whose state this 81 * object represents 82 * @param listeners <code>Set</code> containing zero to many (and only) 83 * <code>StampDispenserListener</code>s 84 */ 85 StampDispenserState returnCoins(StampDispenser stampDispenser, 86 Set listeners) { 87 88 StampDispenserEvent event = new StampDispenserEvent(stampDispenser, 10, 0); 89 fireCoinsReturned(event, listeners); 90 return Has0State.getState(); 91 } 92 }
Listing 4-6. Class Has15State
.
1 package com.artima.examples.stampdispenser.ex3; 2 3 import java.util.Set; 4 5 /** 6 * <code>StampDispenserState</code> subclass that represents 7 * the <em>Has10</em> stamp dispenser state. See the documentation 8 * for class <code>StampDispenserState</code> for a state 9 * transition table that specifies the required behavior of 10 * instances of this class. 11 */ 12 class Has15State extends StampDispenserState { 13 14 /** 15 * The single instance of <code>Has5State</code> 16 */ 17 private static Has15State singleton = new Has15State(); 18 19 /** 20 * Constructs a <code>Has15State</code> instance. This constructor is 21 * private to enable this class to restrict the number 22 * of instances of <code>Has15State</code> to one. I.e., <code>Has15State</code> is 23 * a singleton. 24 */ 25 private Has15State() { 26 } 27 28 /** 29 * Factory method that returns the single instance of 30 * <code>Has15State</code>. 31 */ 32 static StampDispenserState getState() { 33 return singleton; 34 } 35 36 /** 37 * Performs actions appropriate for the state represented by 38 * the class of this object for the add5 message. Returns a 39 * reference to the state object that represents the next state 40 * to transition to as a result of the arrival of this add5 message. 41 * 42 * @param stampDispenser the <code>StampDispenser</code> whose state this 43 * object represents 44 * @param listeners <code>Set</code> containing zero to many (and only) 45 * <code>StampDispenserListener</code>s 46 */ 47 StampDispenserState add5(StampDispenser stampDispenser, 48 Set listeners) { 49 50 StampDispenserEvent event = new StampDispenserEvent(stampDispenser, 0, 0); 51 fireStampDispensed(event, listeners); 52 return Has0State.getState(); 53 } 54 55 /** 56 * Performs actions appropriate for the state represented by 57 * the class of this object for the add10 message. Returns a 58 * reference to the state object that represents the next state 59 * to transition to as a result of the arrival of this add10 message. 60 * 61 * @param stampDispenser the <code>StampDispenser</code> whose state this 62 * object represents 63 * @param listeners <code>Set</code> containing zero to many (and only) 64 * <code>StampDispenserListener</code>s 65 */ 66 StampDispenserState add10(StampDispenser stampDispenser, 67 Set listeners) { 68 69 StampDispenserEvent event = new StampDispenserEvent(stampDispenser, 5, 0); 70 fireStampDispensed(event, listeners); 71 return Has0State.getState(); 72 } 73 74 /** 75 * Performs actions appropriate for the state represented by 76 * the class of this object for the returnCoins message. Returns a 77 * reference to the state object that represents the next state 78 * to transition to as a result of the arrival of this returnCoins message. 79 * 80 * @param stampDispenser the <code>StampDispenser</code> whose state this 81 * object represents 82 * @param listeners <code>Set</code> containing zero to many (and only) 83 * <code>StampDispenserListener</code>s 84 */ 85 StampDispenserState returnCoins(StampDispenser stampDispenser, 86 Set listeners) { 87 88 StampDispenserEvent event = new StampDispenserEvent(stampDispenser, 15, 0); 89 fireCoinsReturned(event, listeners); 90 return Has0State.getState(); 91 } 92 }
In the refactored StampDispenser
shown in Listing 4-7, the currentState
variable,
which was type int
in
the code-heavy StampDispenser
, is changed
to type State
.
The StampDispenser
initializes currentState
with the singleton for Has0State
,
the specified initial state. The StampDispenser
then forwards all
messages received from its client back to current state object, which is responsible
for fulfilling the StampDispenser
's contract. The current State
object also indicates to the StampDispenser
what next state should be,
by returning a State
object from each of its methods.
Listing 4-7. The refactored StampDispenser
.
1 package com.artima.examples.stampdispenser.ex3; 2 3 import java.util.Set; 4 import java.util.HashSet; 5 6 /** 7 * A stamp dispenser that accepts nickels and dimes and dispenses 8 * twenty cent stamps. 9 * 10 * @author Bill Venners 11 */ 12 public class StampDispenser { 13 14 private StampDispenserState currentState = Has0State.getState(); 15 private Set listeners = new HashSet(); 16 17 /** 18 * Constructs a new stamp dispenser with a starting balance of zero. 19 */ 20 public StampDispenser() { 21 } 22 23 /** 24 * Adds the specified stamp dispenser listener to receive stamp dispenser events 25 * from this stamp dispenser. If <code>l</code> is <code>null</code>, no exception 26 * is thrown and no action is performed. If <code>l</code> is already registered 27 * as a listener, no action is performed. 28 */ 29 public synchronized void addStampDispenserListener(StampDispenserListener l) { 30 31 listeners.add(l); 32 } 33 34 /** 35 * Removes the specified stamp dispenser listener so that it no longer 36 * receives stamp dispenser events from this stamp dispenser. This method 37 * performs no function, nor does it throw an exception, if the listener 38 * specified by the argument was not previously added to this 39 * component. If <code>l</code> is <code>null</code>, no exception is 40 * thrown and no action is performed. 41 */ 42 public synchronized void removeStampDispenserListener(StampDispenserListener l) { 43 44 listeners.remove(l); 45 } 46 47 /** 48 * Add 5 cents to the stamp dispenser. If the amount added 49 * causes the current value to become or exceed 20 cents, the price of 50 * a stamp, the stamp will be automatically dispensed. 51 */ 52 public void add5() { 53 54 currentState = currentState.add5(this, listeners); 55 } 56 57 /** 58 * Add 10 cents to the stamp dispenser. If the amount added 59 * causes the current value to become or exceed 20 cents, the price of 60 * a stamp, the stamp will be automatically dispensed. 61 */ 62 public void add10() { 63 64 currentState = currentState.add10(this, listeners); 65 } 66 67 /** 68 * Returns coins. If the balance is zero, no action is 69 * performed. 70 */ 71 public void returnCoins() { 72 73 currentState = currentState.returnCoins(this, listeners); 74 } 75 }
The Go4 book claims that the benefit of the state pattern is that it localizes state specific code. I.e., all
the code for each state is in one place, the concrete subclass for the state. The state
pattern eliminates those unsightly switch statements that have a case for each state.
Such switch statements are difficult to maintain, the Go4 claims, especially if there are many
states.
The StampDispenser
example
demonstrates that the switch statements of the code-heavy version do indeed disappear when refactored with
the state pattern.
The Go4 book does mention the potential maintenance cost of distributing the behavior for
different states across several classes, which increases the number of classes and is less compact
than a single class. But Go4 insists that more classes is better than long switch statements, especially
if there are many states.
To me, the state pattern makes the most sense when the requirements are understood in terms of
a state machine in the first place. For example, when writing a class that deals with
the Java Telephony API, it may make sense to use the state pattern where the states map to
the well-understood and recognized states through which a telephone call progresses. This state
machine, which is shown in Figure 4-1, appears in the Java Telephony specification.
The point of the state pattern is to make code easier to understand and change. If the state machine isn't
well understood, then a state pattern organization of the class won't be either. For example, now
that I've seen the state pattern StampDispenser
, I still prefer the original
version of StampDispenser
shown in Guideline 3. I think the Guideline 3
StampDispenser
is the easiest to
understand and change. So my take on the state pattern is that the state pattern makes
sense in the solution, only in those relatively rare cases in which a state machine
is used to define the requirements. Otherwise, programmers will be scratching their heads
trying to figure out your state machine, which may end up making it more difficult to
understand your class.
The reason I included this example, however, is because I feel the state pattern illustrates well the way in which service-oriented objects act like state machines. I find this state-machine view of objects to be a valuable insight in design. When you design service-oriented objects, you are designing elaborate state machines.
Sponsored Links
|