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.
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
}
}
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).
Client
Client interact withVendingMachine
.VendingMechine
This class holds a number of states. By introducingState
interface, it depends on interface not implementation.State
Provides common interface for allConcreteState
.ConcreteStates
Each state transaction has different behavior depending on the state. For example,insertMoney
method defined in theIdle
class and the one in theOutOfStock
state behave differently.
Now, lots of if statements in VendingMachine
are removed, and state transitions are explicit (You'll see this in implementation).
Structure
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);
}
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");
}
}
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 +
'}';
}
}
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);
}
}
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}
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)