State Pattern

designpatterns
Published

February 29, 2024

The State pattern is a powerful behavioral design pattern that allows an object to alter its behavior when its internal state changes. It’s particularly useful when you have an object with multiple states, and the behavior associated with each state differs significantly. Instead of using complex conditional statements to manage these state-dependent behaviors, the State pattern encapsulates each state in its own class, leading to cleaner, more maintainable code.

Understanding the Problem

Imagine you’re building a simple traffic light system. A traffic light can be in one of three states: red, yellow, or green. Each state dictates a specific action:

  • Red: Stop
  • Yellow: Slow down
  • Green: Go

Without the State pattern, you might end up with a single TrafficLight class containing a large switch statement or a series of if-else conditions to manage the behavior based on the current light color. This approach quickly becomes unwieldy as the number of states and actions increases.

The State Pattern Solution

The State pattern solves this by creating separate classes for each state: RedLight, YellowLight, and GreenLight. Each state class encapsulates the behavior specific to that state. A TrafficLight class then delegates the behavior to the currently active state object.

Let’s implement this in JavaScript:

// State interface
class LightState {
  constructor() {
    if (this.constructor === LightState) {
      throw new Error("Abstract classes can't be instantiated.");
    }
  }
  handle() {
    throw new Error("Method 'handle' must be implemented.");
  }
  change(context) {
    throw new Error("Method 'change' must be implemented.");
  }
}


// Concrete states
class RedLight extends LightState {
  handle() {
    console.log("Stop!");
  }
  change(context) {
    context.setState(new GreenLight());
  }
}

class YellowLight extends LightState {
  handle() {
    console.log("Slow down!");
  }
  change(context) {
    context.setState(new RedLight());
  }
}

class GreenLight extends LightState {
  handle() {
    console.log("Go!");
  }
  change(context) {
    context.setState(new YellowLight());
  }
}

// Context
class TrafficLight {
  constructor(state) {
    this.state = state;
  }

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

  handle() {
    this.state.handle();
    this.state.change(this);
  }
}

// Usage
const trafficLight = new TrafficLight(new RedLight());

trafficLight.handle(); // Output: Stop!
trafficLight.handle(); // Output: Go!
trafficLight.handle(); // Output: Slow down!
trafficLight.handle(); // Output: Stop!

In this example, LightState acts as an interface defining the methods that each concrete state class must implement. The TrafficLight class maintains a reference to the current state object and delegates the handle method call to it. The state transition logic is encapsulated within each state class.

Advantages of the State Pattern

  • Improved code organization: Separates state-specific logic into distinct classes.
  • Enhanced maintainability: Changes to one state don’t affect others.
  • Increased flexibility: Adding new states is relatively easy.
  • Improved testability: Each state can be tested independently.

When to Use the State Pattern

The State pattern is best suited for situations where:

  • An object’s behavior depends on its state.
  • The object has many possible states.
  • State transitions are complex.

This guide provides a foundational understanding of the State pattern in JavaScript. By understanding its principles and applying it appropriately, you can write more elegant, maintainable, and scalable code.