State Design Pattern

The State Pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes.

How can an object alter its behavior 😵??

Let us understand this with the help of an example.

Suppose you want to start an online cab driving service like Uber.
At any point in time, your users can be in one of the following states:

  • 🚖 not booked
  • 🚖 requested
  • 🚖 got assigned
  • 🚖 trip started
  • 🚖 trip completed

Also notice that we move from one state to another, only when some action is performed. For example, we move from “🚖 not booked” to “🚖 requested “ only when the passenger requests a ride.

state changes with performed actions

In our implementation, we also need to make sure that we are not able to perform nonsensical actions like boarding a cab while it’s yet to be assigned 😕.

So lets start our implementation.

class Cab {

    // State constants
    public static final int NOT_BOOKED = 0;
    public static final int REQUESTED  = 1;
    public static final int ASSIGNED   = 2;
    public static final int ON_TRIP    = 3;
    public static final int COMPLETED  = 4;

    // Current state at any point in time
    private int state; 

    public Cab() {
        this.state = NOT_BOOKED;
        System.out.println("App opened: NOT_BOOKED state");
    }

    public void requestRide() {
      // Implement the behavior for each state when passenger requests a ride
    }

    public void assignDriver() {
      // Implement the behavior for each state when system assigns a driver
    }

    public void startTrip() {
      // Implement the behavior for each state when passenger starts trip (boards cab)
    }

    public void endTrip() {
      // Implement the behavior for each state when passenger ends trip
    }
}

Let’s now implement the behavior for each state when passenger requests a ride

// Passenger requests a ride
public void requestRide() {

    if (state == NOT_BOOKED) {
        System.out.println("Ride requested, searching for driver");
        state = REQUESTED;

    } else if (state == REQUESTED) {
        System.out.println("Already searching for a driver");

    } else if (state == ASSIGNED) {
        System.out.println("Driver already assigned");

    } else if (state == ON_TRIP) {
        System.out.println("Already on a trip");

    } else if (state == COMPLETED) {
        System.out.println("Starting new ride request");
        state = REQUESTED;
    }
}

Similarly let’s now implement the behavior for each state when system assigns a driver, passenger boards cab and ends trip

// System assigns a driver
public void assignDriver() {

    if (state == NOT_BOOKED) {
        System.out.println("Cannot assign driver: no ride requested");

    } else if (state == REQUESTED) {
        System.out.println("Driver assigned");
        state = ASSIGNED;

    } else if (state == ASSIGNED) {
        System.out.println("Driver already assigned");

    } else if (state == ON_TRIP) {
        System.out.println("Trip already in progress");

    } else if (state == COMPLETED) {
        System.out.println("Cannot assign: trip already completed");
    }
}

// Passenger starts trip (boards cab)
public void startTrip() {

    if (state == NOT_BOOKED) {
        System.out.println("Cannot start: no booking");

    } else if (state == REQUESTED) {
        System.out.println("Cannot start: driver not assigned yet");

    } else if (state == ASSIGNED) {
        System.out.println("Trip started");
        state = ON_TRIP;

    } else if (state == ON_TRIP) {
        System.out.println("Trip already in progress");

    } else if (state == COMPLETED) {
        System.out.println("Trip already completed");
    }
}

// Passenger ends trip
public void endTrip() {

    if (state == NOT_BOOKED) {
        System.out.println("No trip to end");

    } else if (state == REQUESTED) {
        System.out.println("Cannot end: trip not started");

    } else if (state == ASSIGNED) {
        System.out.println("Cannot end: trip not started");

    } else if (state == ON_TRIP) {
        System.out.println("Trip ended, processing payment");
        state = COMPLETED;

    } else if (state == COMPLETED) {
        System.out.println("Trip already completed");
    }
}

Of course, our current functionalities work perfectly. But just imagine what would happen if tomorrow we need to add a new state 😰??
How much code would we have to touch, and how prone to errors would that make the system 🥶??

Right now, this code is basically a jungle of if-else-if statements 🌴😅. It is hard to read, harder to maintain, and a nightmare to extend when a new state shows up !!

State Design Pattern

We need to refactor this code, so that it’s easy to maintain and modify.
In fact we should try to localize the behavior for each state – so that if we make changes to one state, we don’t run the risk of messing up the other states.

Let’s start by defining a State interface that declares a method for each action our context can perform.

interface CabState {
    void requestRide(Cab cab);
    void assignDriver(Cab cab);
    void startTrip(Cab cab);
    void endTrip(Cab cab);
}

The State interface defines a common interface for all concrete states.
All the states implement the same interface so they are interchangeable.

Let us start with the NOT_BOOKED state.

class NotBookedState implements CabState {

    public void requestRide(Cab cab) {
        System.out.println("Ride requested, searching for driver");
        cab.setState(new RequestedState());
    }

    public void assignDriver(Cab cab) {
        System.out.println("Cannot assign driver: no ride requested");
    }

    public void startTrip(Cab cab) {
        System.out.println("Cannot start: no booking");
    }

    public void endTrip(Cab cab) {
        System.out.println("No trip to end");
    }
}

Similarly let us create the concrete implementations for all the other states.

class RequestedState implements CabState {

    public void requestRide(Cab cab) {
        System.out.println("Already searching for a driver");
    }

    public void assignDriver(Cab cab) {
        System.out.println("Driver assigned");
        cab.setState(new AssignedState());
    }

    public void startTrip(Cab cab) {
        System.out.println("Cannot start: driver not assigned yet");
    }

    public void endTrip(Cab cab) {
        System.out.println("Cannot end: trip not started");
    }
}

class AssignedState implements CabState {

    public void requestRide(Cab cab) {
        System.out.println("Driver already assigned");
    }

    public void assignDriver(Cab cab) {
        System.out.println("Driver already assigned");
    }

    public void startTrip(Cab cab) {
        System.out.println("Trip started");
        cab.setState(new OnTripState());
    }

    public void endTrip(Cab cab) {
        System.out.println("Cannot end: trip not started");
    }
}

class OnTripState implements CabState {

    public void requestRide(Cab cab) {
        System.out.println("Already on a trip");
    }

    public void assignDriver(Cab cab) {
        System.out.println("Trip already in progress");
    }

    public void startTrip(Cab cab) {
        System.out.println("Trip already in progress");
    }

    public void endTrip(Cab cab) {
        System.out.println("Trip ended, processing payment");
        cab.setState(new CompletedState());
    }
}

class CompletedState implements CabState {

    public void requestRide(Cab cab) {
        System.out.println("Starting new ride request");
        cab.setState(new RequestedState());
    }

    public void assignDriver(Cab cab) {
        System.out.println("Cannot assign: trip already completed");
    }

    public void startTrip(Cab cab) {
        System.out.println("Trip already completed");
    }

    public void endTrip(Cab cab) {
        System.out.println("Trip already completed");
    }
}

Concrete states handle requests from the Context. Each concrete state provides its own representation for a request. In this way, when a context changes state, its behavior changes as well.

Umm… what is a context 🧐??

Context is the class that can have a number of internal states. In our example, Cab is the context.

So, let’s now define our context.

class Cab {

    private CabState state;

    public Cab() {
        this.state = new NotBookedState();
        System.out.println("App opened: NOT_BOOKED state");
    }

    public void setState(CabState state) {
        this.state = state;
    }

    public void requestRide() {
        state.requestRide(this);
    }

    public void assignDriver() {
        state.assignDriver(this);
    }

    public void startTrip() {
        state.startTrip(this);
    }

    public void endTrip() {
        state.endTrip(this);
    }
}

And there you go 🥁 …your cab is officially state-savvy, ready to shift states faster than rush-hour traffic changes lanes 🚕💨

If we look back at the original definition, it should now make perfect sense.
The State pattern encapsulates each state into its own class and delegates behavior to the object representing the current state, which is our context.

We can now easily see that by encapsulating state behavior :

  • Every state knows only its behavior
  • No giant if-else
  • Adding a new state means just adding a new class

And with that, we’ve wrapped up the State Design Pattern. You’re now fully equipped with everything you need to know on State Pattern 🥳.