- Modeling States
- Refactoring to STATE
- Making States Constant
- Summary
Refactoring to STATE
The code for Door_1 is somewhat complex because the use of the state variable is spread throughout the class. In addition, you might find it difficult to compare the state transition methods, particularly click(), with the state machine in Figure 22.1. The STATE pattern can help you to simplify this code. To apply STATE in this example, make each state of the door a separate class, as Figure 22.3 shows.
Figure 22.3 This diagram shows a door's states as classes in an arrangement that mirrors the door's state machine.
The refactoring that Figure 22.3 shows uses the Door_2 class to contain the context of the state machine. A context is an object that contains information that is environmental and relevant to a group of other objects. In particular, STATE uses a context object to record which instance of DoorState is the current state.
The DoorState class constructor requires a Door_2 object. Subclasses of DoorState use this object to communicate changes in state back to the door. By giving these classes an attribute that ties them to a specific Door object, this design requires that a DoorState object be referenced by a single Door object. In turn, this requires the Door class to define its states as local variables:
package com.oozinoz.carousel; public class Door_2 extends Observable { public final DoorState CLOSED = new DoorClosed(this); public final DoorState OPENING = new DoorOpening(this); public final DoorState OPEN = new DoorOpen(this); public final DoorState CLOSING = new DoorClosing(this); public final DoorState STAYOPEN = new DoorStayOpen(this); // private DoorState state = CLOSED; // ... }
The abstract DoorState class requires subclasses to implement click(). This is consistent with the state machine, in which every state has a click() transition. The DoorState class stubs out other transitions, so that subclasses can override or ignore irrelevant messages:
public abstract class DoorState { protected Door_2 door; public DoorState(Door_2 door) { this.door = door; } public abstract void click(); public void complete() { } public String status() { String s = getClass().getName(); return s.substring(s.lastIndexOf('.') + 1); } public void timeout() { } }
Note that the status() method works for all the states and is much simpler than its predecessor before refactoring.
Challenge 22.3
The new status() method returns a slightly different description of a door's state. What's the difference?
A solution appears on page 415.
The new design doesn't change the role of a Door object in receiving state changes from the carousel. But now the Door_2 object simply passes these changes to its current state object:
package com.oozinoz.carousel; public class Door_2 extends Observable { // ... (DoorState variables) public void click() { state.click(); }
public void complete() { state.complete(); }
protected void setState(DoorState state) { this.state = state; setChanged(); notifyObservers(); }
public String status() { return state.status(); }
public void timeout() { state.timeout(); } }
The click(), complete(), status(), and timeout() methods show the pure polymorphism of this approach. Each of these methods is still a kind of switch. In each case, the operation is fixed, but the class of the receiverthe class of statevaries. The rule of polymorphism is that the method that executes depends on the operation signature and the class of the receiver. What happens when you call click()? The answer depends on the door's state. The code still effectively performs a switch, but by relying on polymorphism, the code is simpler than before.
The setState() method in the Door_2 class is now used by subclasses of DoorState. These subclasses closely resemble their counterparts in the state machine in Figure 22.1. For example, the code for DoorOpen handles calls to click() and timeout(), the two transitions from the Open state in the state machine:
package com.oozinoz.carousel; public class DoorOpen extends DoorState { public DoorOpen(Door_2 door) { super(door); }
public void click() { door.setState(door.STAYOPEN); }
public void timeout() { door.setState(door.CLOSING); } }
Challenge 22.4
Write the code for DoorClosing.java.
A solution appears on page 415.
The new design leads to much simpler code, but you might feel a bit dissatisfied that the "constants" that the Door class uses are in fact local variables.