The Artima Developer Community
Sponsored Link

Interface Design by Bill Venners
Understand the relationship between mutable Service-Oriented Objects and state machines

Advertisement

Interface Design | Contents | Previous | Next

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.

Show the Java Telephony API state-transition diagram.

Figure 4-1. The state-transition diagram for a telephone call.

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:

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:

Table 4-1. The stamp dispenser state-transition table.

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:



Figure 4-2. The stamp dispenser state-transition diagram.

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



Google
  Web Artima.com   
Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use