Design Patterns By Example: Implementing a State Machine

How do you implement the State pattern, while separating the different concerns? We use an example to discuss how to write code easy to understand and maintain.

When writing code, our classes often go through a series of transformations. What starts out as a simple class will grow as behavior is added. If care is not taken, your code will become difficult to understand and maintain. For example, assume you’re implementing a telephone. First, you support only the simplest of usages: your phone is not connected to the telephone jack. Thus, the phone can be either on-hook or off-hook.

You implement this as a class Telephone with a single boolean member offHook. Your phone has two operations pickUp() and hangUp() which manipulate the boolean. Great. Now you jack up your phone and want to implement the next use-case: the user picks up the phone (after which he should hear a tone) and presses a key (after which the tone should stop). You introduce a new boolean hasPressedFirstKey and a function pressKey(int key).

Boolean-Oriented Programming

Already, you have a couple of problems with your code. First, you have modeled three states (on hook, off hook while waiting for first key to be pressed, off hook while waiting for more keys to be pressed) with two boolean variables. Thus, there is one combination of values that does not correspond to a valid state for your telephone: offHook = false and hasPressedFirstKey = true. The state of a bigger system might involve more variables and might even be scattered over many classes. In that case, “boolean-oriented programming” like this makes the code very hard to understand.

Second, member variables live for as long as the object does. In our telephone example, the variable hasPressedFirstKey makes little sense as soon as the full phone number has been dialed and someone on the other side has answered. So the actual lifetime of hasPressedFirstKey is shorter than its “physical” lifetime. If we strive for self-documenting code, this is pretty far from it. And of course, this is even worse in the case of a complex system. So what can we do about it? State design pattern to the rescue.

The State Design Pattern

The Wikipedia page on the State design pattern says that the purpose of State is to “represent the state of an object”. In our telephony example, we would create three state classes: e.g. OnHookState, OffHookWaitForFirstDigitState, OffHookWaitForMoreDigitsState. State classes will only model valid states of the telephone, removing the first problem from above. Also, since there is always a valid state for our telephone, we remove the lifetime problem from above.

I use the State pattern for two reasons: First, it captures the behavior of the code in a single place. This will make your code easy to understand. Second, it makes it easy for you to separate what the system does (the behavior) from how it’s done (the implementation). This will make your code easy to maintain and test. Let’s go through an example.

States, Events and Actions

We have already mentioned the state classes. They all inherit from a common interface ITelephoneState. The state interface defines the events that the system accepts (here in Java):

interface ITelephoneState {
    void pickUp();
    void hangUp();
    void pressKey(int key);
}

As said earlier, we want the state class to be explicit about the system behavior, but without involving implementation details. Instead of letting the state class contain implementation details, we delegate to an action interface ITelephoneAction (shown later). Let’s implement the OffHookWaitForFirstDigitState class:

class OffHookWaitForFirstDigitState implements ITelephoneState {
    OffHookWaitForFirstDigitState(ITelephoneAction action) {
        this.action = action;
    }
    void pickUp() { /* do nothing */ }
    void hangUp() {
        action.stopTone();
        action.changeState(new OnHookState());
    }
    void pressKey(int key) {
        action.stopTone();
        action.changeState(new OffHookWaitForMoreDigitsState());
    }
    private ITelephoneAction action;
}

We see that the state conforms to the ITelephoneState interface. ITelephoneAction is defined like this:

interface ITelephoneAction {
    void stopTone();
    // ... more telephone specific functions here ...
    void changeState(ITelephoneState newState);
}

Thus, the responsibility of the state class is to implement the behavior (what the system does). The responsibility of the action class is to provide the implementation (how things are done). This makes the state code very easy to read.

The ITelephoneAction interface is normally implemented by the Telephone class:

class Telephone implements ITelephoneAction {
    Telephone() {
        state = new OnHookState(this);  // start state
    }

    // public interface
    public void pickUp() { state.pickUp(); }
    public void hangUp() { state.hangUp(); }
    public void pressKey(int key) { state.pressKey(key); }

    // implements ITelephoneAction
    void stopTone() { /* do something */ }
    void changeState(ITelephoneState newState) {
        state = newState;
    }
}

Note that the state never talks directly to the Telephone class. This ensures that the state uses only what’s needed from Telephone, and not the full range of public functions in Telephone. Furthermore, talking to an interface will allow you to unit test the logic of the state machine without using Telephone. To summarize the interactions between Telephone and its state: the Telephone class talks to the state class through the ITelephoneState interface; the state class talk to the Telephone class through the ITelephoneAction interface.

Trace Logging

As an added bonus, having a clear separation between state, event and action will make it easy for you to implement nice trace logs. If we trace every state change, event function call and action function call, we can use indentation to show the flow through the state machine:

OnHookState
    offHook                      // event
        changeState              // action
OffHookWaitForFirstDigitState    // new state
    pressKey
        stopTone
        changeState
OffHookWaitForMoreDigitsState
    ...

As you see, states are not indented, events are indented one level and actions resulting from the event are indented two levels.

Leave a Reply

Your email address will not be published.