Python State Pattern

The core idea of the State Pattern is to encapsulate an object's state into independent classes and delegate the object's behavior to the current state object. When the object's state changes, it switches to a different state object, thereby changing its behavior.

Why do we need the State Pattern?

Without using the State Pattern, we usually use a large number ofif-elseorswitch-casestatements to handle behavior in different states. This approach has several problems:

  • Code bloat: As the number of states increases, conditional statements become increasingly complex
  • Difficult to maintain: Modifying the behavior of one state may affect other states
  • Violates the Open-Closed Principle: Adding a new state requires modifying existing code

The State Pattern solves these problems by encapsulating each state into an independent class.


Structure of the State Pattern

Main Roles

The State Pattern consists of three core roles:

  1. Context (context): Maintains an instance of a concrete state object; this instance defines the current state
  2. State (state interface): Defines an interface for encapsulating behavior associated with a particular state of the Context
  3. ConcreteState (concrete state): Implements the State interface; each concrete state class implements a behavior related to a state of the Context

UML Class Diagram


Implementation of the State Pattern

Basic Implementation Steps

Let's understand the implementation of the State Pattern through a simple example. Suppose we have an elevator system where the elevator has different states: open, closed, moving, and stopped.

Step 1: Define the State Interface

Example

from abc import ABC, abstractmethod

class ElevatorState(ABC):
    """Elevator state interface"""
   
    @abstractmethod
    def open_doors(self):
        """Open door operation"""
        pass
   
    @abstractmethod
    def close_doors(self):
        """Close door operation"""
        pass
   
    @abstractmethod
    def move(self):
        """Move operation"""
        pass
   
    @abstractmethod
    def stop(self):
        """Stop operation"""
        pass

Step 2: Implement Concrete State Classes

Example

class DoorOpenState(ElevatorState):
    """Open state"""
   
    def open_doors(self):
        print("The door is already open")
        return self
   
    def close_doors(self):
        print("Closing the door...")
        return DoorClosedState()
   
    def move(self):
        print("Error: The door is still open, cannot move")
        return self
   
    def stop(self):
        print("The elevator has stopped")
        return self

class DoorClosedState(ElevatorState):
    """Closed state"""
   
    def open_doors(self):
        print("Opening the door...")
        return DoorOpenState()
   
    def close_doors(self):
        print("The door is already closed")
        return self
   
    def move(self):
        print("The elevator starts moving...")
        return MovingState()
   
    def stop(self):
        print("The elevator has stopped")
        return self

class MovingState(ElevatorState):
    """Moving state"""
   
    def open_doors(self):
        print("Error: The elevator is moving, cannot open the door")
        return self
   
    def close_doors(self):
        print("The door is already closed")
        return self
   
    def move(self):
        print("The elevator is moving")
        return self
   
    def stop(self):
        print("The elevator is stopping...")
        return DoorClosedState()

Step 3: Create the Context Class

Example

class Elevator:
    """Elevator context class"""
   
    def __init__(self):
        # Initial state is closed door state
        self._state = DoorClosedState()
   
    @property
    def state(self):
        """Get current state"""
        return self._state
   
    def set_state(self, state):
        """Set new state"""
        self._state = state
        print(f"State switched to: {state.__class__.__name__}")
   
    def open_doors(self):
        """Open door operation"""
        print("Performing open door operation...")
        self.set_state(self._state.open_doors())
   
    def close_doors(self):
        """Close door operation"""
        print("Performing close door operation...")
        self.set_state(self._state.close_doors())
   
    def move(self):
        """Move operation"""
        print("Performing move operation...")
        self.set_state(self._state.move())
   
    def stop(self):
        """Stop operation"""
        print("Performing stop operation...")
        self.set_state(self._state.stop())

Step 4: Use the State Pattern

Example

# Create elevator instance
elevator = Elevator()

# Test state transitions
print("=== Elevator State Transition Test ===")

elevator.open_doors()   # Switch from closed state to open state
elevator.close_doors()  # Switch back from open state to closed state
elevator.move()         # Switch from closed state to moving state
elevator.stop()         # Switch back from moving state to closed state

print("\n=== Test illegal operation ===)
elevator.open_doors()   # Open the door normally
elevator.move()         # Try to move while the door is open (should raise an error)

Run Result

=== 电梯状态转换测试 ===
执行开门操作...
正在开门...
状态已切换为: DoorOpenState
执行关门操作...
正在关门...
状态已切换为: DoorClosedState
执行移动操作...
电梯开始移动...
状态已切换为: MovingState
执行停止操作...
电梯停止中...
状态已切换为: DoorClosedState

=== 测试非法操作 ===
执行开门操作...
正在开门...
状态已切换为: DoorOpenState
执行移动操作...
错误:门还开着,不能移动
状态已切换为: DoorOpenState

Advanced Applications of the State Pattern

State Pattern with Shared State

In some cases, multiple contexts may need to share the same state object. We can achieve state sharing through the Singleton pattern.

Example

class Singleton(type):
    """Singleton metaclass"""
    _instances = {}
   
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class SharedDoorOpenState(ElevatorState, metaclass=Singleton):
    """Shared open state (Singleton)"""
   
    def open_doors(self):
        print("The door is already open")
        return self
   
    def close_doors(self):
        print("Closing the door...")
        return SharedDoorClosedState()
   
    def move(self):
        print("Error: The door is still open, cannot move")
        return self
   
    def stop(self):
        print("The elevator has stopped")
        return self

class SharedDoorClosedState(ElevatorState, metaclass=Singleton):
    """Shared closed state (Singleton)"""
   
    def open_doors(self):
        print("Opening the door...")
        return SharedDoorOpenState()
   
    def close_doors(self):
        print("The door is already closed")
        return self
   
    def move(self):
        print("The elevator starts moving...")
        return SharedMovingState()
   
    def stop(self):
        print("The elevator has stopped")
        return self

class SharedMovingState(ElevatorState, metaclass=Singleton):
    """Shared moving state (Singleton)"""
   
    def open_doors(self):
        print("Error: The elevator is moving, cannot open the door")
        return self
   
    def close_doors(self):
        print("The door is already closed")
        return self
   
    def move(self):
        print("The elevator is moving")
        return self
   
    def stop(self):
        print("The elevator is stopping...")
        return SharedDoorClosedState()

State Pattern and State Machines

The State Pattern is often used in combination with state machines. Below is a more complex state machine example:

Example

class VendingMachineState(ABC):
    """Vending machine state interface"""
   
    @abstractmethod
    def insert_coin(self, machine, amount):
        """Insert coin"""
        pass
   
    @abstractmethod
    def select_product(self, machine, product):
        """Select product"""
        pass
   
    @abstractmethod
    def dispense(self, machine):
        """Dispense product"""
        pass
   
    @abstractmethod
    def refund(self, machine):
        """Refund"""
        pass

class WaitingState(VendingMachineState):
    """Waiting for coin state"""
   
    def insert_coin(self, machine, amount):
        print(f"Inserted {amount} yuan")
        machine.balance += amount
        return HasMoneyState()
   
    def select_product(self, machine, product):
        print("Please insert coin first")
        return self
   
    def dispense(self, machine):
        print("Please insert coin and select a product first")
        return self
   
    def refund(self, machine):
        print("No amount to refund")
        return self

class HasMoneyState(VendingMachineState):
    """Coin inserted state"""
   
    def insert_coin(self, machine, amount):
        print(f"Continue inserting {amount} yuan")
        machine.balance += amount
        return self
   
    def select_product(self, machine, product):
        if product.price <= machine.balance:
            print(f"Product selected: {product.name}")
            machine.selected_product = product
            return ProductSelectedState()
        else:
            print("Insufficient balance, please continue inserting coins or select a cheaper product")
            return self
   
    def dispense(self, machine):
        print("Please select a product first")
        return self
   
    def refund(self, machine):
        print(f"Refund {machine.balance} yuan")
        machine.balance = 0
        return WaitingState()

class ProductSelectedState(VendingMachineState):
    """Product selected state"""
   
    def insert_coin(self, machine, amount):
        print("Product already selected, cannot insert more coins")
        return self
   
    def select_product(self, machine, product):
        print("Product already selected, please complete the current transaction first")
        return self
   
    def dispense(self, machine):
        product = machine.selected_product
        change = machine.balance - product.price
       
        print(f"Dispensing: {product.name}")
        if change > 0:
            print(f"Change: {change} yuan")
       
        machine.balance = 0
        machine.selected_product = None
        return WaitingState()
   
    def refund(self, machine):
        print(f"Refund {machine.balance} yuan")
        machine.balance = 0
        machine.selected_product = None
        return WaitingState()

class Product:
    """Product class"""
   
    def __init__(self, name, price):
        self.name = name
        self.price = price

class VendingMachine:
    """Vending machine"""
   
    def __init__(self):
        self._state = WaitingState()
        self.balance = 0
        self.selected_product = None
   
    def set_state(self, state):
        self._state = state
   
    def insert_coin(self, amount):
        print(f"Operation: Insert coin {amount} yuan")
        self.set_state(self._state.insert_coin(self, amount))
   
    def select_product(self, product):
        print(f"Operation: Select product {product.name}")
        self.set_state(self._state.select_product(self, product))
   
    def dispense(self):
        print("Operation: Request product dispensing")
        self.set_state(self._state.dispense(self))
   
    def refund(self):
        print("Operation: Request refund")
        self.set_state(self._state.refund(self))

Pros and Cons of the State Pattern

Advantages

  1. Single Responsibility Principle: Put code related to a particular state in independent classes
  2. Open-Closed Principle: Introduce new states without modifying existing state classes and context
  3. Eliminate conditional statements: Eliminate huge conditional branch statements through polymorphic calls
  4. Clear state transitions: Make state transitions more explicit, reducing errors caused by state transitions

Disadvantages

  1. Possible over-design: If the number of states is small or rarely changes, using the State Pattern may be overly complex
  2. Increased number of state classes: Each state requires a corresponding class, which may lead to an increase in the number of classes
  3. Context and State Coupling: The context needs to know all concrete state classes in order to perform state transitions

Practice Exercises

Exercise 1: Improve the Elevator System

Try adding the following features to the elevator system:

  • Add a "Maintenance" state, in which all operations are prohibited
  • Add a floor selection feature, where floors can only be selected in the stopped state
  • Implement automatic state transitions for floor arrival

Exercise 2: Implement a Traffic Light System

Use the State pattern to implement a traffic light system with three states: red light, green light, and yellow light. The state transition rules are as follows:

  • Red light → Green light (after 30 seconds)
  • Green light → Yellow light (after 25 seconds)
  • Yellow light → Red light (after 5 seconds)

Exercise 3: Game Character States

Design a state system for a game character, including the following states:

  • Normal state: can move and attack
  • Poisoned state: continuously loses health, movement speed halved
  • Stunned state: cannot move or attack
  • Invincible state: immune to damage

Summary

The State pattern is a powerful design pattern, particularly suitable for scenarios where an object's behavior depends on its state, and where there are many states with complex transitions. By encapsulating each state as an independent class, the State pattern makes code clearer, easier to maintain, and easier to extend.

Key Points:

  • The State pattern eliminates conditional statements by encapsulating states as objects
  • The context delegates state-related behavior to the current state object
  • State transitions can be triggered by the context or by the state objects themselves
  • Reasonable use of the State pattern can greatly improve code maintainability
Other Extensions