State Design Pattern - Definition

The State Design Pattern is a behavioural design pattern. It allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

In this post, we will explore a sample real world application, the need for state-based transitions, and see how we can use the State design pattern.

Vending machine example

Let us say we have a vending machine to vend items. It has items stocked inside the machine with a UI console that enables users to choose items after paying for it. A typical interaction is like:

  1. The user inserts money into the vending machine.
  2. Chooses the item by entering the item’s id. The id will be shown beneath the item.
  3. The vending machine does some validations like,
    • Validating the entered id
    • Checking for the availability of the item
    • Checking if sufficient money is inserted.
  4. Once all validations succeed, it dispenses the item to the user and collects the money and stores it so that it cannot be taken back by the user.

The vending machine will not accept money if it is totally out of stock (has no items).

States and actions

The above application or use-case can be viewed to have a set of states and actions. At any point of time, the vending machine can be in one of the following states:

  1. It is waiting for money to be inserted
  2. It is waiting for the user to make a selection
  3. Dispensing an item
  4. The vending machine is out of stock

We call these states.

Actions performed by the user take the vending machine from one state to another.

  • When the user inserts money, the vending machine goes from ‘waiting for money’ to ‘waiting for input’ state.
  • After the user makes a choice, it goes from ‘waiting for input’ to ‘dispensing’ state.
  • After it has dispensed the item, it either goes back to ‘waiting for money’ state or waiting for input’ states depending on whether money is remaining or not.

State transitions

We know that the vending machine has a lots of states. The user needs to perform an action on the vending machine to change its state. We call this a state transition.

We can represent the state as circles and show the actions that cause a state transition as labels on the arrows. The state transition for the vending machine use-case is shown below.

StateDesignPattern-StateTransition

A procedural implementation for state transition

Let us first attempt to implement the vending machine’s state transitions in a procedural way. We represent the states using integers.

private static final int WAITING_FOR_MONEY = 1;
private static final int WAITING_FOR_CHOICE = 2;
private static final int DISPENSING = 3;
private static final int VENDING_MACHINE_OUT_OF_STOCK = 4;

private int currentState = WAITING_FOR_MONEY;

There are four states and the machine starts with WAITING_FOR_MONEY state.

We use the below shown POJO for representing an item. An item has an id, the price and the quantity.

public class Item {
    private int itemId;
    private int price;
    private int stock;

    public Item(int itemId, int price, int stock) {
        this.itemId = itemId;
        this.price = price;
        this.stock = stock;
    }

    public int getItemId() {
        return itemId;
    }

    public int getPrice() {
        return price;
    }

    public int getStock() {
        return stock;
    }
}

The vending machine has a list of items and starts off with 0 moneyAvailable.

private static final List<Item> ITEMS = List.of(
        new Item(1, 10, 0),
        new Item(2, 20, 4),
        new Item(3, 30, 3),
        new Item(4, 40, 2)
);

private int moneyAvailable = 0;
private int numberOfItems = ITEMS.stream().mapToInt(Item::getStock).sum();

Inserting money

Let us implement insertMoney action on the vending machine. This action is valid only when the vending machine is in the WAITING_FOR_MONEY state. Performing this action on other states should result in an error. In the blow code, I have printed the error message to the console. In a real vending machine, this will display the message to the user.

public void insertMoney(int amount) {
    if (currentState == WAITING_FOR_MONEY) {
        moneyAvailable += amount;
        currentState = WAITING_FOR_CHOICE;
    } else if (currentState == WAITING_FOR_CHOICE) {
        System.out.println("You have already inserted money");
    } else if (currentState == DISPENSING) {
        System.out.println("One item is being dispensed. Wait for it to complete");
    } else {
        System.out.println("Vending machine is out of stock");
    }
}

It adds the inserted money to the total available money if the current state is WAITING_FOR_MONEY. Then it moves to the next state, WAITING_FOR_CHOICE.

Inserting money when the vending machine is say out of stock or when it is already dispensing it invalid.

Making a choice

The user can make a choice only when the machine is in WAITING_FOR_CHOICE state.

There are quite a few things to check.

First, it validates the inputted id of the item i.e., it checks if an item exists with the inputted id. If not, it shows an error. Second, it checks if the item is in stock. Third, it checks if the user has enough money available to buy that item. At last, once all the above validations succeed, it deducts the price of the item from the available money and reduces the number of items available by one. It then moves the state to DISPENSING state.

public void makeChoice(int itemId) {
    if (currentState == WAITING_FOR_MONEY) {
        System.out.println("You have to enter money first");
    } else if (currentState == WAITING_FOR_CHOICE) {
        Optional<Item> item = ITEMS.stream()
                .filter(i -> i.getItemId() == itemId)
                .findFirst();
        if (!item.isPresent()) {
            System.out.println("Invalid item id entered");
        } else if (item.isPresent() && item.get().getStock() == 0) {
            System.out.println("Item is out of stock");
        } else if (item.isPresent() && moneyAvailable < item.get().getPrice()) {
            System.out.println("Not enough money");
        } else {
            moneyAvailable = moneyAvailable - item.get().getPrice();
            numberOfItems = numberOfItems -1;
            System.out.println("Preparing to dispense");
            currentState = DISPENSING;
        }
    } else if (currentState == DISPENSING) {
        System.out.println("One item is being dispensed. Wait for it to complete");
    } else {
        System.out.println("Vending machine is out of stock");
    }
}

Dispensing an item

The vending machine will dispense an item when it is is DISPENSING state. After it dispenses the item, the vending machine can move to one of three states.

  • If it has run out of items, it moves to VENDING_MACHINE_OUT_OF_STOCK state.
  • If it still has money available, it moves to WAITING_FOR_CHOICE state.
  • Else - it moves to WAITING_FOR_CHOICE state.
public void dispense(int itemId) {
    if (currentState == WAITING_FOR_MONEY) {
        System.out.println("You have to enter money first");
    } else if (currentState == WAITING_FOR_CHOICE) {
        System.out.println("Please make a choice first");
    } else if (currentState == DISPENSING) {
        System.out.println("The item " + itemId  + " is dispensed. Please collect it");
        if (numberOfItems == 0) {
            currentState = VENDING_MACHINE_OUT_OF_STOCK;
        } else if (moneyAvailable > 0) {
            currentState = WAITING_FOR_CHOICE;
        } else {
            currentState = WAITING_FOR_CHOICE;
        }
    } else {
        System.out.println("Vending machine is out of stock");
    }
}

Demo of the imperative state transition

Let us run the vending machine application we have created.

VendingMachine vendingMachine = new VendingMachine();
vendingMachine.makeChoice(1);

Since we are making a choice before inserting money, it will print an error message.

You have to enter money first

Asking for an invalid item:

vendingMachine.insertMoney(20);
System.out.println(vendingMachine.getCurrentState());
vendingMachine.makeChoice(21);

Prints,

2
Invalid item id entered

After we inserted money, it goes to state 2 which is waiting for a choice. But since we made an invalid choice, it printed an error message.

Not enough money:

vendingMachine.makeChoice(3); //Not enough money

We haven’t inserted enough money to buy Item 3.

Buying an item which is out of stock:

vendingMachine.makeChoice(1); //Item is out of stock

Let us see a happy path. We are buying item 2 - it dispenses it and goes back to waiting for money state.

vendingMachine.makeChoice(2);
System.out.println(vendingMachine.getCurrentState());
vendingMachine.dispense(2);
System.out.println(vendingMachine.getCurrentState());
System.out.println();

Outputs,

Preparing to dispense
3
The item 2 is dispensed. Please collect it
1

But if we had more money, it will go back to waiting for choice state.

vendingMachine = new VendingMachine();
vendingMachine.insertMoney(40);
vendingMachine.makeChoice(2);
vendingMachine.dispense(2);
System.out.println(vendingMachine.getCurrentState());
vendingMachine.makeChoice(2);
vendingMachine.dispense(2);
System.out.println(vendingMachine.getCurrentState());
Preparing to dispense
The item 2 is dispensed. Please collect it
2
Preparing to dispense
The item 2 is dispensed. Please collect it
1

Analysis of the imperative approach

Though the above attempt might have worked there are many problems in it.

  1. It is an imperative approach with lots of if..else branches (conditional code). This is very error prone and not readable. It is very easy to make a mistake in one of the conditions.
  2. Adding a new state is not easy. We have to change each of the methods (actions) to accommodate the new state. This violates the OCP Principle.

The State Pattern

To apply the state design pattern, we do,

  • Create a State interface which has one method for each operation. In our example, it will be insertMoney, makeChoice, and dispense.
  • Create one class for each of the states. These classes will implement the State interface.
  • Remove all the ugly conditional code in the vending machine and delegate to the appropriate state class.

Rather than handling each state inside an action, we make classes corresponding to each state and implement the action as methods. This will localize the behaviour and make things easier to understand and change.

We will see each of the steps now.

The State interface

public interface State {
    void insertMoney(int amount);

    Optional<Item> makeChoice(int itemId);

    void dispense(int itemId);
}

Each of the methods are the three actions we had earlier, but there is a small difference. We make the makeChoice method return an Optional Item. We will use this to decide if the vending machine must dispense or not.

The State Context

The Context class is the very important in the State Pattern implementation.

We create an interface for Vending machine and these are the operations that are exposed to the user.

public interface VendingMachine {
    void insertMoney(int amount);
    void makeChoice(int itemId);
}

Note that there is no public method called dispense as dispensing is an internal (private) operation that will be done if certain conditions are met (item exists and enough money is inserted).

The implementation of the VendingMachine is as follows.

public class VendingMachineContext implements VendingMachine {
    private static final List<Item> ITEMS = List.of(
            new Item(1, 10, 0),
            new Item(2, 20, 4),
            new Item(3, 30, 3),
            new Item(4, 40, 2)
    );
    private int moneyAvailable = 0;
    private int numberOfItems = ITEMS.stream().mapToInt(Item::getStock).sum();

    private final WaitingForMoneyState waitingForMoneyState;
    private final WaitingForChoiceState waitingForChoiceState;
    private final DispensingState dispensingState;
    private final OutOfStockState outOfStockState;
    private State currentState = null;

    public VendingMachineContext() {
        this.waitingForMoneyState = new WaitingForMoneyState(this);
        this.waitingForChoiceState = new WaitingForChoiceState(this);;
        this.dispensingState = new DispensingState(this);;
        this.outOfStockState = new OutOfStockState(this);;

        this.currentState = waitingForMoneyState;
    }

    public int getMoneyAvailable() {
        return moneyAvailable;
    }

    public void setMoneyAvailable(int moneyAvailable) {
        this.moneyAvailable = moneyAvailable;
    }

    public List<Item> getItems() {
        return ITEMS;
    }

    public int getNumberOfItems() {
        return numberOfItems;
    }

    public void setNumberOfItems(int numberOfItems) {
        this.numberOfItems = numberOfItems;
    }

    public State getCurrentState() {
        return currentState;
    }

    public void setCurrentState(State currentState) {
        this.currentState = currentState;
    }

    public WaitingForMoneyState getWaitingForMoneyState() {
        return waitingForMoneyState;
    }

    public WaitingForChoiceState getWaitingForChoiceState() {
        return waitingForChoiceState;
    }

    public DispensingState getDispensingState() {
        return dispensingState;
    }

    public OutOfStockState getOutOfStockState() {
        return outOfStockState;
    }

    
    public void insertMoney(int amount) {
        currentState.insertMoney(amount);
    }

    public void makeChoice(int itemId) {
        Optional<Item> item = currentState.makeChoice(itemId);
        if (item.isPresent()) {
            currentState.dispense(itemId);
        }
    }
}

It creates the individual State classes (shown later). The vending machine context object has a getter method to return the current state the vending machine is in. It also has getter methods to return the individual states. These will be used by the State classes.

Concrete State classes

Each of the State subclasses will have a reference to the VendingMachineContext class. The VendingMachineContext is shown in the next section. That context class is the main vending machine interface to the client.

Let us implement the WaitingForMoney State.

public class WaitingForMoneyState implements State {
    private final VendingMachineContext vendingMachine;

    public WaitingForMoneyState(VendingMachineContext vendingMachine) {
        this.vendingMachine = vendingMachine;
    }

    @Override
    public void insertMoney(int amount) {
        int moneyAvailable = vendingMachine.getMoneyAvailable();
        vendingMachine.setMoneyAvailable(moneyAvailable + amount);
        vendingMachine.setCurrentState(vendingMachine.getWaitingForChoiceState());
    }

    @Override
    public Optional<Item> makeChoice(int itemId) {
        System.out.println("You have to enter money first");
        return Optional.empty();
    }

    @Override
    public void dispense(int itemId) {
        System.out.println("You have to enter money first");
    }
}

This is similar to what we have seen. Each of the conditional legs that dealt with WAITING_FOR_MONEY is grouped into a class. But what is interesting is the state transition. We will see this soon.

The other state implementations are shown below.

public class WaitingForChoiceState implements State {
    private final VendingMachineContext vendingMachine;

    public WaitingForChoiceState(VendingMachineContext vendingMachine) {
        this.vendingMachine = vendingMachine;
    }

    @Override
    public void insertMoney(int amount) {
        System.out.println("You have already inserted money");
    }

    @Override
    public Optional<Item> makeChoice(int itemId) {
        List<Item> items = vendingMachine.getItems();
        int moneyAvailable = vendingMachine.getMoneyAvailable();
        int numberOfItems = vendingMachine.getNumberOfItems();
        Optional<Item> item = items.stream()
                .filter(i -> i.getItemId() == itemId)
                .findFirst();
        if (!item.isPresent()) {
            System.out.println("Invalid item id entered");
        } else if (item.isPresent() && item.get().getStock() == 0) {
            System.out.println("Item is out of stock");
        } else if (item.isPresent() && moneyAvailable < item.get().getPrice()) {
            System.out.println("Not enough money");
        } else {
            vendingMachine.setMoneyAvailable(moneyAvailable - item.get().getPrice());
            vendingMachine.setNumberOfItems(numberOfItems - 1);
            System.out.println("Preparing to dispense");
            vendingMachine.setCurrentState(vendingMachine.getDispensingState());
            return item;
        }
        return Optional.empty();
    }

    @Override
    public void dispense(int itemId) {
        System.out.println("Please make a choice first");
    }
}
public class DispensingState implements State {
    private final VendingMachineContext vendingMachine;

    public DispensingState(VendingMachineContext vendingMachine) {
        this.vendingMachine = vendingMachine;
    }

    @Override
    public void insertMoney(int amount) {
        System.out.println("One item is being dispensed. Wait for it to complete");
    }

    @Override
    public Optional<Item> makeChoice(int itemId) {
        System.out.println("One item is being dispensed. Wait for it to complete");
        return Optional.empty();
    }

    @Override
    public void dispense(int itemId) {
        System.out.println("The item " + itemId  + " is dispensed. Please collect it");
        int numberOfItems = vendingMachine.getNumberOfItems();
        int moneyAvailable = vendingMachine.getMoneyAvailable();
        if (numberOfItems == 0) {
            vendingMachine.setCurrentState(vendingMachine.getOutOfStockState());
        } else if (moneyAvailable > 0) {
            vendingMachine.setCurrentState(vendingMachine.getWaitingForChoiceState());
        } else {
            vendingMachine.setCurrentState(vendingMachine.getWaitingForMoneyState());
        }
    }
}
public class OutOfStockState implements State {
    private final VendingMachineContext vendingMachine;

    public OutOfStockState(VendingMachineContext vendingMachine) {
        this.vendingMachine = vendingMachine;
    }

    @Override
    public void insertMoney(int amount) {
        System.out.println("Vending machine is out of stock");
    }

    @Override
    public Optional<Item> makeChoice(int itemId) {
        System.out.println("Vending machine is out of stock");
        return Optional.empty();
    }

    @Override
    public void dispense(int itemId) {
        System.out.println("Vending machine is out of stock");
    }
}

Interaction between the states and the context

State changes

The code for the State subclasses involve state transition. They call the setter method on the context (VendingMachineContext) to set the next state the vending machine must transition to.

Example: In the WaitingForMoneyState, once it receives a money, it updates the moneyAvailable on the vending machine. Then, it sets the state of the vending machine to WaitingForChoiceState by,

vendingMachine.setCurrentState(vendingMachine.getWaitingForChoiceState());

Similarly, in WaitingForChoiceState, once the user has made a valid choice, it moves to dispensing state by,

vendingMachine.setCurrentState(vendingMachine.getDispensingState());

This is how the state objects work with the state context object to make the state transitions.

Delegating action to a state

Now, from the vending machine standpoint, it doesn’t really care what the current state is. It just delegates the action to the current state, and it takes care of what to do.

For example, when it calls currentState.insertMoney(amount), if the current state is WaitingForMoneyState then it follows the happy path and moves to WaitingForChoiceState. If we call insertMoney when the vending machine is in say DispensingState, then the delegate will call the insertMoney on DispensingState class which will print the error message.

The Context class is not concerned with the state transitions but is handled by the concrete State classes. Thus when the state changes, the behaviour changes.

Advantages of the State Design Pattern

  • All state specific logic is in one place. This makes it easier to understand and easier to add new state.
  • Removes the need for conditional branches.
  • It enables atomic state changes. A state change is just the change in the currentState reference assignment.

If the state classes involve no internal state, it is possible to share the state objects.

Disadvantages of the State Design Pattern

  • Increases the number of classes.
  • Leads to complex coupling among the states and between the states and the context. Since the state objects do the transition, one state has to know about all the possible next states. In our example, the dispense state could go to one of three states. The state class depends on a lot of internal data of the context class exposed via getter methods.

Structure of the State Design Pattern

StateDesignPatternClassDiagram

Participants

Context (VendingMachineContext):

  • It defines the contract to the clients.
  • Has an instance of State to refer to the current state.
  • It also has references to all possible states.

State (State):

  • Defines an interface for encapsulating the behaviour associated with a state.

Concrete subclasses (WaitingForMoneyState, WaitingForChoiceState, DispensingState, and OutOfStockState):

  • Each subclass of State implements all the operations or behaviours associated with a state.

Conclusion

We started with a real-world vending machine application and implemented it in a procedural way. Then, we saw the problems it leads to, and we fixed it by applying the State Design Pattern.

You can read other design patterns here.

References