DEV Community

Sota
Sota

Posted on

State Pattern

What is State Pattern?

State pattern is a behavioral pattern that allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

When to use it?

  • Use State pattern when you have an object whose behavior is different depending on its current state.
  • Use State pattern when you want to implement system that represents finite automata.

Problem

We are developing vending machine system. Here's the graph that shows the flow of how vending machine works.

Image description

If you've studied computer science, you may notice it looks like Finite Automata (if you don't know what is FA, no worries, you don't need to know about it to understand State pattern). In the graph, circles represent states and arrows represent state transaction.

Anyway, let's begin to code our first version of system.

public class VendingMachine {

    // Vending machine has four states
    final static int OUT_OF_STOCK = 0;
    final static int IDLE = 1;
    final static int HAS_MONEY = 2;
    final static int DRINK_SOLD = 3;

    // When vending machine is placed, it has no drink stocks
    int state = OUT_OF_STOCK; // Our initial state
    int stocks = 0;

    public VendingMachine(int stocks) {
        this.stocks = stocks;
        if (stocks > 0) {
            state = IDLE;
        }
    }

    public void insertMoney() {
        if (state == OUT_OF_STOCK) {
            System.out.println("ERROR: Drink is out of stock");
        } else if (state == IDLE) {
            System.out.println("SUCCESS: You inserted money");
            state = HAS_MONEY;
        } else if (state == HAS_MONEY) {
            System.out.println("ERROR: You already inserted money, you can buy now");
        } else if (state == DRINK_SOLD) {
            System.out.println("ERROR: Please wait, we're dispensing you a drink that you bought");
        }
    }

    public void pressBuyButton() {
        // if statements for each states
    }

    public void pressCancelButton() {
        // if statements for each states
    }

    public void dispenseDrink() {
        // if statements for each states
    }

    public void refill() {
        // if statements for each states
    }
}
Enter fullscreen mode Exit fullscreen mode

Can you spot the problems?

  • This code violates open-closed principle.
  • This code violates single responsibility principle.
  • This code doesn't encapsulate what varies.
  • State transitions are not explicit, they are placed under the conditional statements.
  • If you add other states or state transitions, it is likely to cause bugs in already working code.

Solution

So, how do we tackle those problems? We are going to turn each state into an object, then implement methods representing state transition from that specific state (object).

Image description

  1. Client
    Client interact with VendingMachine.

  2. VendingMechine
    This class holds a number of states. By introducing State interface, it depends on interface not implementation.

  3. State
    Provides common interface for all ConcreteState.

  4. ConcreteStates
    Each state transaction has different behavior depending on the state. For example, insertMoney method defined in the Idle class and the one in the OutOfStock state behave differently.

Now, lots of if statements in VendingMachine are removed, and state transitions are explicit (You'll see this in implementation).

Structure

Image description

Does this structure remind you of another pattern? Yes, this class diagram is fundamentally the same as Strategy pattern, but their intents differ. We'll discuss it later.

Implementation in Java

// Declare all possible state transition methods
public interface State {
    void insertMoney();
    void pressBuyButton();
    void pressCancelButton();
    void dispenseDrink();
    void refill(int stocks);
}
Enter fullscreen mode Exit fullscreen mode
public class DrinkSold implements State {

    VendingMachine machine;

    public DrinkSold(VendingMachine machine) {
        this.machine = machine;
    }

    @Override
    public void insertMoney() {
        System.out.println("ERROR: no state transition occurs");
    }

    @Override
    public void pressBuyButton() {
        System.out.println("ERROR: no state transition occurs");
    }

    @Override
    public void pressCancelButton() {
        System.out.println("ERROR: no state transition occurs");
    }

    @Override
    public void dispenseDrink() {
        machine.customerTakesDrink();
        if (machine.getStocks() > 0) {
            System.out.println("SUCCESS: DrinkSold -> Idle");
            machine.setState(machine.getIdle());
        } else {
            System.out.println("SUCCESS: DrinkSold -> OutOfStock");
            machine.setState(machine.getOutOfStock());
        }
    }

    @Override
    public void refill(int stocks) {
        System.out.println("ERROR: no state transition occurs");
    }
}
Enter fullscreen mode Exit fullscreen mode

I'll skip other concrete states because they are similar, but you can check them in my GitHub repo (link to my repo is at the end of this blog).

public class VendingMachine {

    private State idle;
    private State hasMoney;
    private State drinkSold;
    private State outOfStock;

    private State currentState;
    int stocks = 0;

    public VendingMachine(int stocks) {
        idle = new Idle(this);
        hasMoney = new HasMoney(this);
        drinkSold = new DrinkSold(this);
        outOfStock = new OutOfStock(this);

        this.stocks = stocks;
        if (stocks > 0) {
            currentState = idle;
        } else {
            currentState = outOfStock;
        }
    }

    // State transition methods, actual implementation is delegated to concrete states
    public void insertMoney() {
        currentState.insertMoney();
    }

    public void pressBuyButton() {
        currentState.pressBuyButton();
        currentState.dispenseDrink();
    }

    public void pressCancelButton() {
        currentState.pressCancelButton();
    }

    public void refill(int stocks) {
        currentState.refill(stocks);
    }

    // Method to be used by concrete states to move one state to another
    public void setState(State state) {
        currentState = state;
    }

    // Helper method used when dispensing a drink on DrinkSold state
    public void customerTakesDrink() {
        if (stocks > 0) {
            System.out.println("Customer grab a drink");
            stocks--;
        }
    }

    // Getter for each state
    public State getIdle() {
        return idle;
    }

    public State getHasMoney() {
        return hasMoney;
    }

    public State getDrinkSold() {
        return drinkSold;
    }

    public State getOutOfStock() {
        return outOfStock;
    }

    public int getStocks() {
        return stocks;
    }

    public void setStocks(int stocks) {
        this.stocks = stocks;
    }

    @Override
    public String toString() {
        return "VendingMachine {" +
                "currentState: " + currentState.getClass().getSimpleName() +
                ", stocks: " + stocks +
                '}';
    }
}
Enter fullscreen mode Exit fullscreen mode
public class VendingMachineTestDrive {

    public static void main(String[] args) {
        VendingMachine machine = new VendingMachine(2);

        System.out.println(machine);

        System.out.println("-- Customer insert money and cancel the transaction --");
        machine.insertMoney();
        machine.pressCancelButton();

        System.out.println("-- Customer insert money and buy a drink --");
        machine.insertMoney();
        machine.pressBuyButton();

        System.out.println(machine);

        System.out.println("-- Customer insert money and buy a drink --");
        machine.insertMoney();
        machine.pressBuyButton();

        System.out.println(machine);

        System.out.println("-- Customer insert money --");
        machine.insertMoney();

        System.out.println("-- Owner is going to refill drinks --");
        machine.refill(2);
        System.out.println(machine);
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

VendingMachine {currentState: Idle, stocks: 2}
-- Customer insert money and cancel the transaction --
SUCCESS: Idle -> HasMoney
SUCCESS: HasMoney -> Idle
-- Customer insert money and buy a drink --
SUCCESS: Idle -> HasMoney
SUCCESS: HasMoney -> DrinkSold
Customer grab a drink
SUCCESS: DrinkSold -> Idle
VendingMachine {currentState: Idle, stocks: 1}
-- Customer insert money and buy a drink --
SUCCESS: Idle -> HasMoney
SUCCESS: HasMoney -> DrinkSold
Customer grab a drink
SUCCESS: DrinkSold -> OutOfStock
VendingMachine {currentState: OutOfStock, stocks: 0}
-- Customer insert money --
ERROR: no state transition occurs
-- Owner is going to refill drinks --
SUCCESS: OutOfStock -> Idle
VendingMachine {currentState: Idle, stocks: 2}
Enter fullscreen mode Exit fullscreen mode

Pitfalls

  • State pattern tends to end up having lots of classes because you'll have as many classes as states exist. If some states are almost identical, you could combine them together. This way, we reduce duplicate code but violate single responsibility principle, remember they are trade-off when you use State pattern.
  • If there are many reachable states from one state, it will be more complicated to implement.

Comparison with Strategy Pattern

  • They both allows objects to incorporate different behavior through composition and delegation.
  • In Strategy pattern, "Client" changes object's behavior at runtime. On the other hand, in State pattern, "Context" changes states according to well-defined state transition methods.

You can check all the design pattern implementations here.
GitHub Repository

Top comments (0)