State Machine Design pattern — Part 2: State Pattern vs. State Machine

Kousik Nath
Jan 20 · 6 min read

In the last post, we talked about using State Machine to build state-oriented systems to solve several business problems. Before we start building any proper state machine example, it’s better if we explore other alternatives & discuss their pros & cons. It will help us to properly realise the potential of State Machine design patterns.

Problem Statement:

Let’s consider a very simple version of an Uber trip life cycle. The life cycle consists of the following states & transitions as described in the image below. Let’s find out different approaches to build this state-oriented system.

The Basic Approach:

The intuitive approach that comes into mind first is to handle states & transitions through simple if else. But this approach does not scale, with every new state / transition addition / deletion, you need to change the big block of if else / switch statements that drive the whole logic. Refer to the below code to identify how much messy the code looks & just imagine what happens when the code base grows massively 😮. This approach can work with extremely static transitions & states, but that chance is very rare. You should avoid this method as it would become a huge maintenance overhead. Example Code:

void manageStatesAndTransitions(Event event, InputData data) {
State nextState = getNextState(event, data);

switch(nextState) {
case State.TRIP_REQUESTED:
handleTripRequest(event, data);
break;

case State.PAYMENT:
handlePayment(event, data);
break;

case State.DRIVER_ASSIGNED:
handleDriverAllocation(event, data);
break;

case State.DRIVER_CANCELLED:
handleTripCancellationByDriver(event, data);
break;

case State.CUSTOMER_CANCELLED:
handleTripCancellationByCustomer(event, data);
break;
}
}

void handleTripRequest(Event event, InputData data) {

if(event == Event.trip_requested) {
// Check pre-conditions
// do some work if needed...

manageStatesAndTransitions(Event.payment_requested, data);
} else if(event == Event.payment_failed) {
showBookingError();
}
}

void handlePayment(Event event, InputData data) {
if(event == Event.payment_requested) {
Payment payment = doPayment(data);

if(payment.isSuccess()) {
manageStatesAndTransitions(Event.payment_success, data);
} else {
manageStatesAndTransitions(Event.payment_failed, data);
}
} else if(event == Event.payment_failed) {
manageStatesAndTransitions(Event.payment_failed, data);
} else if(event == Event.payment_success) {
manageStatesAndTransitions(Event.driver_assigned, data);
}
}

State Pattern Approach:

State pattern is one of the behavioural design patterns devised by Gang Of Four. In this pattern, the concerned object holds internal state which can change & the object’s behaviour changes accordingly.

Characteristics:

  1. The state-specific behaviours are defined in different classes & the original object delegates the execution of the behaviour to the current state’s object implementation.
  2. States trigger state transitions from one state to another.
  3. All states implement a common interface that defines all the possible behaviours / actions.

Let’s model the Uber trip states through this mechanism below:

  1. We will define an interface which represents the contract of a state. All states will implement these methods which dictate the behaviour of the object at a certain state. If a method is not applicable in a particular state, the state will ignore defining any action in that method:
interface State
{
void handleTripRequest();
void handlePaymentRequest();
void handleDriverCancellation();
void handleCustomerCancellation();
void completeTrip(); // Driver completes trip, Unassign the driver.
void endTrip(); // After driver is unassigned, do driver & customer rating, take feedback etc.
}

2. We will do a concrete implementation of different states.
TripRequested state:
This is the initial state when customer requests for a trip. It implements the handleTripRequest method and after successful initiation, it sets the state to Payment. So this state indirectly calls Payment state.

class TripRequested implements State {

UberTrip context;

public TripRequested(UberTrip ctx) {
this.context = ctx;
}


void handleTripRequest() {
if(!context.tripStarted()) {
context.setState(context.getPaymentRequestedState());
}
}


void handlePaymentRequest() {
System.out.println("This state just handles initiation of trip request, it does not handle payment");
}


void handleDriverCancellation() {
System.out.println("This state just handles initiation of trip request, it does not handle cancellation");
}


void handleCustomerCancellation() {
System.out.println("This state just handles initiation of trip request, it does not handle cancellation");
}


void completeTrip() {
System.out.println("This state just handles initiation of trip request, it does not handle trip completion");
}


void endTrip() {
System.out.println("This state just handles initiation of trip request, it does not handle ending trip.");
}
}

Payment state:
It handles payment request, success & failure states. On success, it sets the trip’s state to DriverAssigned, on failure, it sets the trip’s state to TripRequested.

class Payment implements State {

UberTrip context;

public PaymentRequested(UberTrip ctx) {
this.context = ctx;
}


void handleTripRequest() {
System.out.println("Payment state does not handle trip initiation request.");
}


void handlePaymentRequest() {
Payment payment = doPayment();

if(payment.isSuccess()) {
context.setState(context.getDriverAssignedState()); // Call driver assigned state.
} else {
context.setState(context.getTripRequestedState()); // Call trip requested state
}
}


void handleDriverCancellation() {
System.out.println("Payment state just handles payment, it does not handle cancellation");
}


void handleCustomerCancellation() {
System.out.println("Payment state just handles payment, it does not handle cancellation");
}


void completeTrip() {
System.out.println("Payment state just handles payment, it does not handle trip completion");
}


void endTrip() {
System.out.println("Payment state just handles payment, it does not handle ending trip.");
}
}

DriverAssigned state:
When assigned driver cancels the trip, the trip’s state is set to TripRequested state so that a new trip request starts automatically. When the driver completes the trip, the trip’s state is changed to DriverUnAssigned state.

class DriverAssigned implements State {

UberTrip context;

public DriverAssigned(UberTrip ctx) {
this.context = ctx;
}


void handleTripRequest() {
System.out.println("Driver Assigned state does not handle trip initiation request.");
}


void handlePaymentRequest() {
System.out.println("Driver Assigned state does not handle payment request.");
}


void handleDriverCancellation() {
context.setState(context.getTripRequestedState()); // If driver cancels, go back to trip requested state & try again.
}


void handleCustomerCancellation() {
System.out.println("Driver Assigned state does not handle customer cancellation.");
}


void completeTrip() {
context.setState(context.getDriverUnAssignedState()); // Call driver unassigned state
}


void endTrip() {
System.out.println("Driver Assigned state does not handle ending trip.");
}
}

CustomerCancelled state:
When a customer cancels the trip, a new trip request is not automatically retried, rather, the trip’s state is set to DriverUnAssigned state.

class CustomerCancelled implements State {

UberTrip context;

public CustomerCancelled(UberTrip ctx) {
this.context = ctx;
}


void handleTripRequest() {
System.out.println("Customer Cancelled state does not handle trip initiation request.");
}


void handlePaymentRequest() {
System.out.println("Customer Cancelled state does not handle payment request.");
}


void handleDriverCancellation() {
System.out.println("Customer Cancelled state does not handle driver cancellation.");
}


void handleCustomerCancellation() {
context.setState(context.getDriverUnassignedState()); // If customer cancels, umassign the driver & related stuffs.
}


void completeTrip() {
System.out.println("Customer Cancelled state does not handle trip completion.");
}


void endTrip() {
System.out.println("Customer Cancelled state does not handle ending trip.");
}
}

Further, DriverUnAssigned state can handle customer / driver rating & feedback accordingly & moves trip’s state to TripEnd state. It’s not shown in the code here.

UberTrip class:
This is the class that describes all possible actions on the trip. It manages an internal state which gets set by individual state objects. UberTrip delegates the behaviour to individual state objects.

class UberTrip {
State tripRequestedState;
State paymentState;
State driverAssignedState;
State driverUnassignedState;
State customerCancelledState;

// CurrentState
State state;

public UberTrip() {
tripRequestedState = new TripRequested(this);
paymentState = new Payment(this);
driverAssignedState = new DriverAssigned(this);
driverUnassignedState = new DriverUnAssigned(this);
customerCancelledState = new CustomerCancelled(this);
}

public setState(State st) {
this.state = st;
}

public getState() {
return this.state;
}

public void requestTrip() {
state.handleTripRequest();
}

public void doPayment() {
state.handlePaymentRequest();
}

public void driverCancelled() {
state.handleDriverCancellation();
}

public void customerCancelled() {
state.handleCustomerCancellation();
}

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

Pros & Cons of State Pattern:

There is no explicit transition defined in this system. Transitions are handled by the states themselves. States can define checks based on some parameters to validate whether it can call the next state or not. This is quite a messy way to implement state-based systems, transitions are still tightly coupled with the states & states take the responsibility to call the next state by setting the next state in the context object ( here the UberTrip object ). So logically a state object handles its own behaviour & next possible transitions — multiple responsibilities. With more new states & transitions, the code base might become junk. Also, all the state objects implement common behaviours through the interface which really seems unnecessary & extra work in real life development. This pattern is better than the basic if else / switch based approach in the way that here you think about decomposing your application process into states & divide behaviours into multiple states, but since transitions are implicitly handled by states themselves, this method is not scalable & in real life you might end up violating Open Closed — Open for extension & closed for Modification principal.

In the next post, we will discuss implementing a proper state machine through Spring State Machine.

Data Driven Investor

from confusion to clarity, not insanity

Kousik Nath

Written by

Deep discussions on problem solving, distributed systems, computing concepts, real life systems designing. Developer @PayPal. https://in.linkedin.com/in/kousikn

Data Driven Investor

from confusion to clarity, not insanity

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade