State - Programming in the Large with Design Patterns (2012)

Programming in the Large with Design Patterns (2012)

Chapter 6. State

Introduction

If you want to turn heads at the lake, drive up and then in with an Amphicar.

Figure 53 Amphicar entering a lake (Photo Credit: Chris McEvoy)

Sometimes described as the fastest car on the water and the fastest boat on the road, the Amphicar is the world’s only civilian mass-produced amphibious automobile.

The mechanics are simple. Torque from the engine is routed through a special land/water transmission to the rear wheels or twin propellers mounted just under the rear bumper. The front tires steer the car on land as well as in the water.

How would you model such a conveyance?

You could lump all the behavior in one class Amphicar. Or, if your programming language supports multiple inheritance, you could store boat behavior in one class and car behavior in another and inherit from both:

However, neither of these solutions captures the true nature of the vehicle. An Amphicar is both a car and a boat, but it's not both at the same time. On land it acts like a car and in the water it acts like boat. During normal use it may switch back and forth several times between being a car and being a boat. A better way of modeling the behavior of this vehicle is with the State design pattern.

Intent

Objects have state and behavior. Objects change their state based on internal and external events. If an object goes through clearly identifiable states, and the object's behavior is especially dependent on its state, it is a good candidate for the State design pattern.

Solution

The solution is to encapsulate state-specific behavior into separate objects and have the context (the object that goes through identifiable states) delegate state-specific requests to these objects.

Figure 54 Structure diagram for State design pattern

A separate concrete state class is created for each identifiable state of the context. State-specific behavior is encapsulated in these classes. The context defines the interface of interest to clients. It keeps a reference to the state object for the current state of the context. State-dependent requests from clients are forwarded to the current state object. State-independent requests from clients are handled locally in the context. Either the context or the concrete state objects are responsible for transitioning from one state to another.

The following class diagram shows how the behavior of an Amphicar might be modeled using the State design pattern.

Figure 55 Conceptual design for an Amphicar

An Amphicar can be in one of two states: Car or Boat. An Amphicar’s behavior depends on its current state. While on land, Amphicar will keep a reference to an instance of Car and act like a car. While on water, Amphicar will keep a reference to an instance of Boat and act like a boat.

Clients make certain requests of an Amphicar (ie turn(), accelerate(), decelerate()). State-dependent behavior such as accelerating and decelerating are forwarded to the object representing the current state. State-independent behaviors such as turning left and right are handled locally in Amphicar (remember, the front wheels of an Amphicar steer both on land and in the water).

Sample Code

Imagine you have been asked to design the software for a simple cell phone with just 4 keys: SND, END, Side Key Up and Side Key Down.

Because cell phones have limited space for physical keys, the same keys are often mapped to different functions depending on the current state or mode of the phone (standby, talking, application running, etc.). For the phone in this example, assume the four keys are mapped to the following functions:

SND Key - When in standby mode, this key will cause the phone to transition into call mode.

END Key - When in call mode or application mode (running an application), this key will cause the phone to transition into standby mode.

Side Key Up/Down - When in standby mode, this key will raise or lower ringer volume. When in call mode, this key will raise or lower voice volume. When in application mode, this key will scroll the display up or down.

Based on the description, the phone appears to be an ideal candidate for the State design pattern. It has clearly identifiable states or modes and the behavior of certain keys depends on the current state of the phone.

One of the best ways to get a complete and accurate picture of the problem you are about to solve with the State design pattern is to model the proposed system with a state machine diagram. Figure 56 shows the state machine diagram for the proposed cell phone.

Figure 56 State machine diagram for a cell phone

The state machine diagram in Figure 56 shows the phone can be in one of three states: Standby, Call or Application. It also shows the events (labels on arrows) that trigger an activity (e.g. increase ringer volume) or cause a transition from one state to another.

Once you have a state machine diagram for a proposed application, transforming it into a design based on the State design pattern is straightforward.

Figure 57 shows the resulting class diagram for a software design based on the State diagram in Figure 56.

Figure 57 Cell phone design based on the State design pattern

The abstract class PhoneState defines an operation for each event in the state machine diagram. A separate concrete subclass is created for each state in the state machine diagram (Standby, Call and Application). These subclasses encapsulate state-specific behavior.

The following code shows the implementation details. In this particular example, state objects control the timing of state transitions. Changes in state are made by calling CellPhone’s changeState() operation.

public class StateExample {

public static void main(String[] args) {

CellPhone phone = new CellPhone();

phone.sideKeyUp();

phone.launchApp();

phone.sideKeyUp();

// Test error checking.

// The following request is invalid

// for the current state

phone.SND_Key();

phone.END_Key();

phone.sideKeyUp();

}

}

class CellPhone {

private PhoneState state;

public CellPhone() {

state = new Standby();

}

public void sideKeyUp() {

state.sideKeyUp();

}

public void sideKeyDown() {

state.sideKeyDown();

}

public void SND_Key() {

state.SND_Key(this);

}

public void END_Key() {

state.END_Key(this);

}

public void launchApp() {

state.launchApp(this);

}

// This method has package visibility.

// Only internal classes can affect a

// change in the state of the context.

void changeState(PhoneState state) {

this.state = state;

}

}

abstract class PhoneState {

// Default behavior for all events is to

// signal an error.

public void sideKeyUp() {

System.out.println("Play Error Sound");

}

public void sideKeyDown() {

System.out.println("Play Error Sound");

}

// A reference to CellPhone is needed because

// this operation may request a state change.

public void SND_Key(CellPhone phone) {

System.out.println("Play Error Sound");

}

public void END_Key(CellPhone phone) {

System.out.println("Play Error Sound");

}

public void launchApp(CellPhone phone) {

System.out.println("Play Error Sound");

}

}

// Concrete State

class Standby extends PhoneState {

public void sideKeyUp() {

System.out.println("Increase ringer volume");

}

public void sideKeyDown() {

System.out.println("Decrease ringer volume");

}

public void SND_Key(CellPhone phone) {

phone.changeState(new Call());

}

public void launchApp(CellPhone phone) {

phone.changeState(new Application());

}

}

class Call extends PhoneState {

public void sideKeyUp() {

System.out.println("Increase voice volume");

}

public void sideKeyDown() {

System.out.println("Decrease voice volume");

}

public void END_Key(CellPhone phone) {

phone.changeState(new Standby());

}

}

class Application extends PhoneState {

public void sideKeyUp() {

System.out.println("Scroll up");

}

public void sideKeyDown() {

System.out.println("Scroll down");

}

public void END_Key(CellPhone phone) {

phone.changeState(new Standby());

}

}

Discussion

One issue to consider when applying the State design pattern is whether to make the context or individual state objects responsible for state transitions. If the criterion for transitioning between states is not a function of state, the most logical choice is to make the context responsible for state transitions. For example, in the implementation of Amphicar the context is responsible for switching between states because state transitions are state-independent. The transition from state A to state B doesn’t depend on current state A.

In the cell phone example, individual state objects (subclasses of PhoneState) are responsible for state transitions. Putting state transition logic in state objects is reasonable for this application because state transitions occur as a consequence of receiving certain events while in certain states. For example, if the SND event is received while in the Standby state the Standby state object will transition the context to the Call state. Otherwise, the event is ignored. Doing the transition in the state object avoids state-checking conditional logic in the context object.

Allowing state objects to take responsibility for state transitions increases coupling between state objects (predecessor state must know about successor state) but generally results in code that is easier to understand and maintain.

Related Patterns

State and Strategy have the same static structure (class diagrams) but differ in intent. The difference between the two patterns does, however, show up in their prototypical runtime behavior (sequence diagrams).

With State, clients have little or no knowledge of concrete state objects. The context typically decides the initial state and associated concrete state object. The context together with the state objects are responsible for transitions between states. State is also more dynamic with the context going through possibly many state changes during its lifetime.

With Strategy, clients are usually aware of different strategy objects and take responsibility for initializing the context with a specific strategy. Although clients can change a context’s strategy at runtime, the configured strategy typically lasts for the lifetime of the context.

The Singleton design pattern can be used to create and manage state objects. If state classes are made Singleton classes, you don’t have to create a new state object during every transition. Instead you can use the Instance() method of the state class to access the unique instance of the state class. For example:

public void launchApp(CellPhone phone) {

phone.changeState(Application.Instance());

}