Design Patterns: State Pattern

These notes were adapted from various readings detailed in the References section.


The State Pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

The State Pattern is a useful pattern to help objects control their behavior by changing internal state. The State Pattern is fundamentally an implementation of a state diagram, or model. To model a state diagram, we need to define all the states – from the start to the terminating state. Then, for every possible action, we may convert this action as a state, and define what parameters are needed to reach that state.‌

From an implementation perspective, we can think of each state name as an instance variable for some class to read so that it can perform some action.‌

We are responsible for building some high-tech Gumball machine. We have the following states the achieve a functional gumball machine:

enum States {
    SOLD_OUT = 0,
    NO_QUARTER = 1,
    HAS_QUARTER = 2,
    SOLD = 3
}

Then, we can create a simple class that has a method to behave like a state machine:

class GumballMachine {
    private state;
    
    public insertQuarter() {
        if (state === States.HAS_QUARTER) {
            log("You cannot insert another quarter.");
        } else if (state === States.SOLD_OUT) {
            log("You cannot insert a quarter, the machine is sold out");
        } else if (state === States.SOLD) {
            log("Please wait, the Gumball machine is giving you a gumball.");
        } else if (state === States.QUARTER) {
            state = States.HAS_QUARTER;
            log("You have inserted a quarter!");
        }
    }
}

By calling insertQuarter, if the Gumball machine is in the correct state, it will transition to a new state called States.HAS_QUARTER. Of course, the other implemented methods can do state transitions too. For example, we may have the Gumball machine be able to have the user turnCrank, ejectQuarter, and finally have itself dispense. These all follow the same type of pattern and structure as insertQuarter.

This type of pattern works pretty well in most cases. However, what if we want to build a new state into the Gumball machine? Suppose now we want to add a new state where the machine can spit out 2 gumballs at once at a 10% chance.

If we want to use the current model, we would have to do a few things to make it work. First, we have to introduce a new state called States.DISPENSE_EXTRA, and for all methods in the gumball machine, account for this new state! That is a lot of work, and we do not want to be forced to modify existing code.

However, what we have now is a great start, so let’s build off of it.

What we can do now is put each state’s behavior into its own class, and every state may implement its own actions. Each state will be responsible for executing its own behavior, and actions when appropriate.

What are the steps?

  1. Define a State interface that contains a method for every action in the Gumball machine.
  2. Then, every state will be a State derived class (implementing the interface). The classes will be responsible for the behavior of the machine when it is in the corresponding state.
interface State {
    insertQuarter();
    ejectQuarter();
    turnCrank();
    dispense();
}

The above tells us that each state will handle each of these actions accordingly. For example, if the state is currently in SoldOutState, then it may handle insertQuarter by displaying an error saying that the machine is sold out.

Okay, so let’s see how other states may be implemented.

class SoldState implements State {
    public insertQuarter() { ... }
    public ejectQuarter() { ... }
    public turnCrank() { ... }
    public dispense() { ... }
}

class SoldOutState implements State {
    private machine; 
    
    constructor(gumballMachine) {
        this.machine = gumballMachine;
    }
    
    public insertQuarter() { ... }
    public ejectQuarter() { ... }
    public turnCrank() { ... }
    public dispense() { ... }
}

class NoQuarterState implements State {
    private machine; 
    
    constructor(gumballMachine) {
        this.machine = gumballMachine;
    }
    
    public insertQuarter() { ... }
    public ejectQuarter() { ... }
    public turnCrank() { ... }
    public dispense() { ... }
}

class HasQuarterState implements State {
    private machine; 
    
    constructor(gumballMachine) {
        this.machine = gumballMachine;
    }
    
    public insertQuarter() { ... }
    public ejectQuarter() { ... }
    public turnCrank() { ... }
    public dispense() { ... }
}

class DispenseExtraState implements State {
    private machine; 
    
    constructor(gumballMachine) {
        this.machine = gumballMachine;
    }
    
    public insertQuarter() { ... }
    public ejectQuarter() { ... }
    public turnCrank() { ... }
    public dispense() { ... }
}

Now, the Gumball machine can read each individual state as a simple State object since each state implements the common interface. Each state receives the instance of the gumball machine, and will use that reference to perform various actions.

How does the Gumball machine then look like in code? First, the Gumball machine has to hold instances of all the states, and the current state. From here, it just needs to call the method in the current state it is holding based on the method that is being invoked by the customer.

class GumballMachine {
    private soldOutState;
    private noQuarterState;
    private hasQuarterState;
    private soldState;
    private dispenseExtraState;
    
    private state;
    private totalGumballs;
    
    public constructor(initialGumballs) {
        this.soldOutState = new SoldOutState(this);
        this.noQuarterState = new NoQuarterState(this);
        this.hasQuarterState = new HasQuarterState(this);
        this.soldState = new SoldState(this);
        this.dispenseExtraState = new DispenseExtraState(this);
        
        this.totalGumballs = initialGumballs;
        if (this.totalGumballs > 0) {
            this.state = this.noQuarterState;
        }
    }
    
    public insertQuarter() {
        this.state.insertQuarter();
    }
    
    public ejectQuarter() {
        this.state.ejectQuarter();
    }
    
    // ... and so on...
}

Each state should perform one single type of function, or have a single type of behavior. It will make it easy to debug, and make sense of functionality.

References

  • Head First Design Patterns - Eric Freeman & Elisabeth Freeman - https://www.oreilly.com/library/view/head-first-design/0596007124/