|
|
|
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
|