State - kgleong/software-engineering GitHub Wiki

Motivation

The State design pattern allows an entity to neatly encapsulate state-based behavior.

A naive approach is to establish an Enum to represent the current state of an object, and vary behavior based on the state via conditionals or switch statements.

This can easily become messy and become unmaintainable.

Also, having separate classes creates well-defined states, rather than relying on an Enum member variable that conditional statements examine.

Instead, the State pattern establishes different classes to represent each state.

The entity, referred to as the context, contains a reference to an object that implements a State interface, which changes classes as the context moves through its lifecycle.

Code

public class Context {
    State mState;

    public void handleRequest() {
        System.out.println("Request received by Context.");
        mState.handle(this);
    }

    public void setState(State state) {
        mState = state;
    }
}

External clients send requests via the handleRequest() method.

The Context class then delegates the request to its currently associated State object.

Different State subclasses are allowed to provide different behavior within their handle() method.

The example above allows the State instances to manage state transitions. This decentralizes transition rules and lets each State object specify its successor State.

This requires the State object to be able to change the Context's state, which requires a setState() method on the Context class.

If transition rules are static, it may be warranted to let the Context manage the state transitions itself.

public abstract class State {
    public abstract void handle(Context context);

    public static void changeState(Context context, State state) {
        context.setState(state);
    }
}

The changeState() method allows a State to transition a Context to another state.

public class ConcreteStateA implements State {
    @Override
    public void handle(Context context) {
        System.out.println("ConcreteStateA handling request.");
        changeState(context, new ConcreteStateB());
    }
}

public class ConcreteStateB implements State {
    @Override
    public void handle(Context context) {
        System.out.println("ConcreteStateB handling request.");
    }
}

Above, ConcreteStateA handles the request, creates the successive State instances and transitions the Context.

Creating and destroying State objects as needed in this manner is one approach.

If States are being changed rapidly and reusing objects makes more sense, then it might be more prudent to make State classes provide Singleton instances.

// Client usage
State initialState = new ConcreteStateA();
Context context = new Context();
context.setState(intialState);

context.handleRequest(); // State change
context.handleRequest();

// Output:
// Request received by Context.
// ConcreteStateA handling request.
// ConcreteStateB handling request.

Use Case

A typical interactive drawing program will allow the user to:

  • Select an area on the canvas.
  • Draw a line.
  • Enter text.
public class DrawingController {
    Tool mTool;

    public void mousePressed() {
        mTool.handleMousePressed();
    }

    public void mouseReleased() {
        mTool.handleMouseReleased();
    }

    public void characterEntered(Char character) {
        mTool.handleCharacter(character);
    }

    public void setTool(Tool tool) {
        mTool = tool;
    }
}

The DrawingController takes in user input and passes it to the currently selected Tool, which are analogous to State instances.

Tool instances are selected by the user via a GUI that calls the setTool() method on the DrawingController.

public interface Tool {
    void handleMousePressed();
    void handleMouseReleased();
    void handleCharacter(Char character);
}

Each Tool represents a different function of the drawing application, allowing each subclass to handle user input differently.

public class SelectionTool implements Tool {
    void handleMousePressed() {
        // Initialize new selection area.
    }

    void handleMouseReleased() {
        // Calculate selection area and highlight on screen.
    }

    void handleCharacter(Char character) {
        // Change mode.  E.g., select rectangle, circle, path.
    }
}

public class TextTool implements Tool {
    void handleMousePressed() {
        // Initialize new text area.
    }

    void handleMouseReleased() {
        // Calculate size of text area and display cursor.
    }

    void handleCharacter(Char character) {
        // Render character to screen at cursor location.
    }
}

Each drawing function is contained within a single class, making it easy to dynamically swap Tool instances in and out.

References