State Pattern – Head First Approach

Alyaa TalaatAlyaa Talaat
5 min read

The State Pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes. It appears as though the object is modifying its class by transitioning between different states.


What is the State Pattern?

Definition: The State Pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class as it delegates behavior to different state classes.

The State Pattern helps eliminate complex conditional logic and instead uses state-specific classes to manage behavior, enhancing code maintainability and flexibility.


Problem Scenario

Imagine you’re developing a vending machine. The machine has different behaviors depending on its state, such as waiting for a coin, dispensing an item, or being out of stock. Managing these state transitions can become complex with conditional checks all over the code. You need a way to manage these behaviors independently and transition between them easily.


Using the State Pattern

Let’s see how to implement the State Pattern using a vending machine example from the Head First Design Patterns book.

Step 1: Define the State Interface

The state interface defines a common way to handle actions for various states.

public interface State {
    public void insertQuarter();
    public void ejectQuarter();
    public void turnCrank();
    public void dispense();
}

Step 2: Create Concrete State Classes

Each concrete state class will implement behavior specific to that state. Here’s a couple of state implementations for a vending machine.

public class NoQuarterState implements State {
    GumballMachine gumballMachine;

    public NoQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    public void insertQuarter() {
        System.out.println("You inserted a quarter");
        gumballMachine.setState(gumballMachine.getHasQuarterState());
    }

    public void ejectQuarter() {
        System.out.println("You haven't inserted a quarter");
    }

    public void turnCrank() {
        System.out.println("You turned, but there's no quarter");
    }

    public void dispense() {
        System.out.println("You need to pay first");
    }
}

public class SoldState implements State {
    GumballMachine gumballMachine;

    public SoldState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    public void insertQuarter() {
        System.out.println("Please wait, we're already giving you a gumball");
    }

    public void ejectQuarter() {
        System.out.println("Sorry, you already turned the crank");
    }

    public void turnCrank() {
        System.out.println("Turning twice doesn’t get you another gumball!");
    }

    public void dispense() {
        gumballMachine.releaseBall();
        if (gumballMachine.getCount() > 0) {
            gumballMachine.setState(gumballMachine.getNoQuarterState());
        } else {
            System.out.println("Oops, out of gumballs!");
            gumballMachine.setState(gumballMachine.getSoldOutState());
        }
    }
}

Step 3: Implement the Context Class

The context class, GumballMachine, maintains a reference to the current state and delegates behavior to the state classes.

public class GumballMachine {
    State soldOutState;
    State noQuarterState;
    State hasQuarterState;
    State soldState;

    State state = soldOutState;
    int count = 0;

    public GumballMachine(int numberGumballs) {
        soldOutState = new SoldOutState(this);
        noQuarterState = new NoQuarterState(this);
        hasQuarterState = new HasQuarterState(this);
        soldState = new SoldState(this);
        this.count = numberGumballs;

        if (numberGumballs > 0) {
            state = noQuarterState;
        }
    }

    public void insertQuarter() {
        state.insertQuarter();
    }

    public void ejectQuarter() {
        state.ejectQuarter();
    }

    public void turnCrank() {
        state.turnCrank();
        state.dispense();
    }

    void releaseBall() {
        System.out.println("A gumball comes rolling out the slot...");
        if (count != 0) {
            count = count - 1;
        }
    }

    // Other methods...

    void setState(State state) {
        this.state = state;
    }

    public State getNoQuarterState() {
        return noQuarterState;
    }

    public State getSoldOutState() {
        return soldOutState;
    }

    // Getters for other states...
}

Step 4: Client Code

The client interacts with the GumballMachine class, which delegates to the correct state to handle the behavior.

public class GumballMachineTest {
    public static void main(String[] args) {
        GumballMachine gumballMachine = new GumballMachine(5);

        gumballMachine.insertQuarter();
        gumballMachine.turnCrank();

        gumballMachine.insertQuarter();
        gumballMachine.turnCrank();
    }
}

Output:

You inserted a quarter
A gumball comes rolling out the slot...
You inserted a quarter
A gumball comes rolling out the slot...

Here, the GumballMachine manages transitions between different states, and the states encapsulate the behavior specific to each state.


Expanding the State Pattern

Adding More States

You can easily extend the State Pattern by adding more states, such as a state for out-of-stock gumballs, by simply creating another concrete state class and adjusting the GumballMachine logic to transition to it when necessary.


Benefits of the State Pattern

  • Simplifies Complex State Transitions: The State Pattern removes the need for complex if-else or switch statements and places state-specific behavior into separate classes.

  • Encapsulates State-Specific Behavior: Each state handles its own behavior, making it easier to manage and modify.

  • Supports State Transitions: Transitions between states are explicit and handled by the context class, improving code readability and flexibility.


When to Use the State Pattern

  • Objects with Multiple States: Use the State Pattern when an object needs to change its behavior based on its internal state.

  • Avoiding Complex Conditionals: If your code has many conditional statements based on state, the State Pattern simplifies it by delegating to state classes.

  • Improving Maintainability: The pattern organizes state-specific behavior into separate classes, improving maintainability and scalability.


Bullet Points

  • The State Pattern encapsulates behavior for different states in separate classes, allowing an object to change its behavior when its state changes.

  • It’s a great way to eliminate complex conditional logic.

  • The context class handles state transitions and delegates behavior to the current state class.


Conclusion

The State Pattern is a powerful tool for managing an object’s behavior across different states. By encapsulating state-specific logic into separate classes, it provides a clean and scalable way to handle state transitions and avoid complex conditional code.


0
Subscribe to my newsletter

Read articles from Alyaa Talaat directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alyaa Talaat
Alyaa Talaat

As a continuous learner, I’m always exploring new technologies and best practices to enhance my skills in software development. I enjoy tackling complex coding challenges, whether it's optimizing performance, implementing new features, or debugging intricate issues.