State Design Pattern Tutorial

Introduction

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

This pattern:

  • Encapsulates state-specific behavior into separate classes
  • Makes state transitions explicit
  • Eliminates complex conditional statements
  • Makes it easy to add new states without changing existing code
  • Promotes the Single Responsibility Principle

At its core, the State pattern organizes code related to particular states into separate classes and delegates state-specific work to instances of these classes. This makes the code more maintainable and easier to extend.

Real-World Analogy: Vending Machine

Imagine a vending machine that behaves differently based on its current state:

  1. Idle State: Waiting for money
    • Display: “Insert coins”
    • Actions: Accept coins → Move to HasMoney state
    • Invalid: Dispense product, return money
  2. HasMoney State: Money inserted
    • Display: “Select product”
    • Actions: Select product → Move to Dispensing state
    • Actions: Cancel → Return money, move to Idle state
    • Invalid: Insert more money without selection
  3. Dispensing State: Dispensing product
    • Display: “Dispensing…”
    • Actions: Dispense product → Move to Idle state
    • Invalid: Insert money, select product
  4. OutOfStock State: No products available
    • Display: “Out of stock”
    • Actions: Refund money → Move to Idle state
    • Invalid: All other actions

The key insight here is that the vending machine’s behavior changes completely based on its state. Instead of having one large class with many if-else statements checking the current state, we create separate classes for each state.

Benefits of this approach:

  • Each state class focuses on one state’s behavior
  • Adding new states doesn’t require changing existing states
  • State transitions are explicit and clear
  • Eliminates complex conditional logic
  • Makes the system easier to understand and maintain

State Pattern Structure

The State pattern typically consists of:

  1. Context: Maintains a reference to the current state and delegates state-specific behavior to it
  2. State Interface: Defines methods that all concrete states must implement
  3. Concrete States: Implement state-specific behavior and handle state transitions
  4. Client: Interacts with the Context, not directly with states

Python Implementation

Let’s implement a simple example of the State pattern in Python:

from abc import ABC, abstractmethod

# State interface
class State(ABC):
    @abstractmethod
    def insert_coin(self, context):
        pass
    
    @abstractmethod
    def select_product(self, context):
        pass
    
    @abstractmethod
    def dispense(self, context):
        pass
    
    @abstractmethod
    def cancel(self, context):
        pass

# Concrete States
class IdleState(State):
    def insert_coin(self, context):
        print("Coin inserted. Please select a product.")
        context.set_state(HasMoneyState())
    
    def select_product(self, context):
        print("Please insert coins first.")
    
    def dispense(self, context):
        print("Please insert coins first.")
    
    def cancel(self, context):
        print("Nothing to cancel.")

class HasMoneyState(State):
    def insert_coin(self, context):
        print("Coin already inserted. Please select a product.")
    
    def select_product(self, context):
        print("Product selected. Dispensing...")
        context.set_state(DispensingState())
        context.dispense()
    
    def dispense(self, context):
        print("Please select a product first.")
    
    def cancel(self, context):
        print("Returning coins.")
        context.set_state(IdleState())

class DispensingState(State):
    def insert_coin(self, context):
        print("Please wait, dispensing in progress.")
    
    def select_product(self, context):
        print("Please wait, dispensing in progress.")
    
    def dispense(self, context):
        print("Product dispensed. Thank you!")
        context.set_state(IdleState())
    
    def cancel(self, context):
        print("Cannot cancel while dispensing.")

class OutOfStockState(State):
    def insert_coin(self, context):
        print("Out of stock. Returning coins.")
    
    def select_product(self, context):
        print("Out of stock.")
    
    def dispense(self, context):
        print("Out of stock.")
    
    def cancel(self, context):
        print("Nothing to cancel.")

# Context
class VendingMachine:
    def __init__(self):
        self._state = IdleState()
        self._inventory = 10
    
    def set_state(self, state):
        self._state = state
    
    def insert_coin(self):
        self._state.insert_coin(self)
    
    def select_product(self):
        if self._inventory > 0:
            self._state.select_product(self)
        else:
            print("Out of stock!")
            self.set_state(OutOfStockState())
    
    def dispense(self):
        self._state.dispense(self)
        if self._inventory > 0:
            self._inventory -= 1
    
    def cancel(self):
        self._state.cancel(self)
    
    def get_inventory(self):
        return self._inventory

# Client code
if __name__ == "__main__":
    machine = VendingMachine()
    
    print("=== Scenario 1: Normal Purchase ===")
    machine.insert_coin()
    machine.select_product()
    
    print("\n=== Scenario 2: Cancel Transaction ===")
    machine.insert_coin()
    machine.cancel()
    
    print("\n=== Scenario 3: Invalid Actions ===")
    machine.select_product()  # No coin inserted
    machine.dispense()  # No coin inserted

Output:

=== Scenario 1: Normal Purchase ===
Coin inserted. Please select a product.
Product selected. Dispensing...
Product dispensed. Thank you!

=== Scenario 2: Cancel Transaction ===
Coin inserted. Please select a product.
Returning coins.

=== Scenario 3: Invalid Actions ===
Please insert coins first.
Please insert coins first.

More Complex Example: Document Workflow System

Let’s implement a more sophisticated example with a document approval workflow:

from abc import ABC, abstractmethod
from datetime import datetime
from enum import Enum

class DocumentStatus(Enum):
    DRAFT = "draft"
    PENDING_REVIEW = "pending_review"
    APPROVED = "approved"
    REJECTED = "rejected"
    PUBLISHED = "published"

class Document:
    def __init__(self, title, author):
        self.title = title
        self.author = author
        self.content = ""
        self.created_at = datetime.now()
        self.updated_at = datetime.now()
        self.reviewer = None
        self.rejection_reason = None
        self._state = DraftState()
    
    def set_state(self, state):
        self._state = state
        self.updated_at = datetime.now()
    
    def get_status(self):
        return self._state.get_status()
    
    def edit(self, content):
        self._state.edit(self, content)
    
    def submit_for_review(self):
        self._state.submit_for_review(self)
    
    def approve(self, reviewer):
        self._state.approve(self, reviewer)
    
    def reject(self, reviewer, reason):
        self._state.reject(self, reviewer, reason)
    
    def publish(self):
        self._state.publish(self)
    
    def __str__(self):
        return f"Document: {self.title} | Status: {self.get_status().value} | Author: {self.author}"

# State interface
class DocumentState(ABC):
    @abstractmethod
    def get_status(self):
        pass
    
    @abstractmethod
    def edit(self, document, content):
        pass
    
    @abstractmethod
    def submit_for_review(self, document):
        pass
    
    @abstractmethod
    def approve(self, document, reviewer):
        pass
    
    @abstractmethod
    def reject(self, document, reviewer, reason):
        pass
    
    @abstractmethod
    def publish(self, document):
        pass

# Concrete States
class DraftState(DocumentState):
    def get_status(self):
        return DocumentStatus.DRAFT
    
    def edit(self, document, content):
        document.content = content
        print(f"✓ Document edited: {document.title}")
    
    def submit_for_review(self, document):
        if not document.content:
            print("✗ Cannot submit empty document for review")
            return
        document.set_state(PendingReviewState())
        print(f"✓ Document submitted for review: {document.title}")
    
    def approve(self, document, reviewer):
        print("✗ Cannot approve a draft document")
    
    def reject(self, document, reviewer, reason):
        print("✗ Cannot reject a draft document")
    
    def publish(self, document):
        print("✗ Cannot publish a draft document")

class PendingReviewState(DocumentState):
    def get_status(self):
        return DocumentStatus.PENDING_REVIEW
    
    def edit(self, document, content):
        print("✗ Cannot edit document while under review")
    
    def submit_for_review(self, document):
        print("✗ Document is already under review")
    
    def approve(self, document, reviewer):
        document.reviewer = reviewer
        document.set_state(ApprovedState())
        print(f"✓ Document approved by {reviewer}: {document.title}")
    
    def reject(self, document, reviewer, reason):
        document.reviewer = reviewer
        document.rejection_reason = reason
        document.set_state(RejectedState())
        print(f"✗ Document rejected by {reviewer}: {document.title}")
        print(f"  Reason: {reason}")
    
    def publish(self, document):
        print("✗ Cannot publish document before approval")

class ApprovedState(DocumentState):
    def get_status(self):
        return DocumentStatus.APPROVED
    
    def edit(self, document, content):
        print("✗ Cannot edit approved document. Create a new version instead.")
    
    def submit_for_review(self, document):
        print("✗ Document is already approved")
    
    def approve(self, document, reviewer):
        print("✗ Document is already approved")
    
    def reject(self, document, reviewer, reason):
        print("✗ Cannot reject an approved document")
    
    def publish(self, document):
        document.set_state(PublishedState())
        print(f"✓ Document published: {document.title}")

class RejectedState(DocumentState):
    def get_status(self):
        return DocumentStatus.REJECTED
    
    def edit(self, document, content):
        document.content = content
        document.rejection_reason = None
        document.set_state(DraftState())
        print(f"✓ Document edited and moved back to draft: {document.title}")
    
    def submit_for_review(self, document):
        print("✗ Please edit the document first")
    
    def approve(self, document, reviewer):
        print("✗ Cannot approve a rejected document")
    
    def reject(self, document, reviewer, reason):
        print("✗ Document is already rejected")
    
    def publish(self, document):
        print("✗ Cannot publish a rejected document")

class PublishedState(DocumentState):
    def get_status(self):
        return DocumentStatus.PUBLISHED
    
    def edit(self, document, content):
        print("✗ Cannot edit published document. Create a new version instead.")
    
    def submit_for_review(self, document):
        print("✗ Document is already published")
    
    def approve(self, document, reviewer):
        print("✗ Document is already published")
    
    def reject(self, document, reviewer, reason):
        print("✗ Cannot reject a published document")
    
    def publish(self, document):
        print("✗ Document is already published")

# Client code
if __name__ == "__main__":
    print("=== Document Workflow System ===\n")
    
    # Create a document
    doc = Document("API Design Guidelines", "Alice")
    print(f"Created: {doc}\n")
    
    # Edit in draft
    doc.edit("# API Design Guidelines\n\n## Introduction\n...")
    print(f"Status: {doc}\n")
    
    # Try to publish without review
    doc.publish()
    print()
    
    # Submit for review
    doc.submit_for_review()
    print(f"Status: {doc}\n")
    
    # Try to edit while under review
    doc.edit("More content")
    print()
    
    # Reject the document
    doc.reject("Bob", "Needs more examples")
    print(f"Status: {doc}\n")
    
    # Edit after rejection
    doc.edit("# API Design Guidelines\n\n## Introduction\n...\n\n## Examples\n...")
    print(f"Status: {doc}\n")
    
    # Submit again
    doc.submit_for_review()
    print(f"Status: {doc}\n")
    
    # Approve
    doc.approve("Bob")
    print(f"Status: {doc}\n")
    
    # Publish
    doc.publish()
    print(f"Status: {doc}\n")
    
    # Try to edit published document
    doc.edit("Cannot edit this")

Advanced Example: TCP Connection States

from abc import ABC, abstractmethod
import time

class TCPState(ABC):
    @abstractmethod
    def open(self, connection):
        pass
    
    @abstractmethod
    def close(self, connection):
        pass
    
    @abstractmethod
    def acknowledge(self, connection):
        pass
    
    @abstractmethod
    def send_data(self, connection, data):
        pass

class ClosedState(TCPState):
    def open(self, connection):
        print("Opening connection...")
        connection.set_state(ListenState())
    
    def close(self, connection):
        print("Connection is already closed")
    
    def acknowledge(self, connection):
        print("Cannot acknowledge in closed state")
    
    def send_data(self, connection, data):
        print("Cannot send data: connection is closed")

class ListenState(TCPState):
    def open(self, connection):
        print("Connection is already open")
    
    def close(self, connection):
        print("Closing connection...")
        connection.set_state(ClosedState())
    
    def acknowledge(self, connection):
        print("Received SYN, sending SYN-ACK...")
        connection.set_state(SynReceivedState())
    
    def send_data(self, connection, data):
        print("Cannot send data: waiting for connection")

class SynReceivedState(TCPState):
    def open(self, connection):
        print("Connection handshake in progress")
    
    def close(self, connection):
        print("Aborting connection...")
        connection.set_state(ClosedState())
    
    def acknowledge(self, connection):
        print("Received ACK, connection established!")
        connection.set_state(EstablishedState())
    
    def send_data(self, connection, data):
        print("Cannot send data: handshake not complete")

class EstablishedState(TCPState):
    def open(self, connection):
        print("Connection is already established")
    
    def close(self, connection):
        print("Initiating connection close...")
        connection.set_state(FinWait1State())
    
    def acknowledge(self, connection):
        print("ACK received")
    
    def send_data(self, connection, data):
        print(f"Sending data: {data}")
        connection.bytes_sent += len(data)

class FinWait1State(TCPState):
    def open(self, connection):
        print("Cannot open: closing in progress")
    
    def close(self, connection):
        print("Already closing")
    
    def acknowledge(self, connection):
        print("Received FIN-ACK, connection closed")
        connection.set_state(ClosedState())
    
    def send_data(self, connection, data):
        print("Cannot send data: connection closing")

class TCPConnection:
    def __init__(self, name):
        self.name = name
        self._state = ClosedState()
        self.bytes_sent = 0
    
    def set_state(self, state):
        self._state = state
        print(f"[{self.name}] State changed to: {state.__class__.__name__}")
    
    def open(self):
        self._state.open(self)
    
    def close(self):
        self._state.close(self)
    
    def acknowledge(self):
        self._state.acknowledge(self)
    
    def send_data(self, data):
        self._state.send_data(self, data)

# Client code
if __name__ == "__main__":
    print("=== TCP Connection State Machine ===\n")
    
    conn = TCPConnection("Client-Server")
    
    # Establish connection
    conn.open()
    conn.acknowledge()
    conn.acknowledge()
    
    print()
    
    # Send data
    conn.send_data("Hello, Server!")
    conn.send_data("How are you?")
    
    print()
    
    # Close connection
    conn.close()
    conn.acknowledge()
    
    print(f"\nTotal bytes sent: {conn.bytes_sent}")

How State Pattern Handles OOP Principles

The State pattern exemplifies several key OOP principles:

1. Single Responsibility Principle

  • Each state class has one responsibility: handling behavior for that specific state
  • Context class focuses on maintaining state and delegating operations
  • No single class tries to handle all states

2. Open/Closed Principle

  • System is open for extension (new states can be added)
  • But closed for modification (existing states don’t need to change)
  • Adding new states doesn’t require changing existing code

3. Encapsulation

  • State-specific behavior is encapsulated in state classes
  • Context doesn’t need to know implementation details of states
  • State transitions are managed internally

4. Polymorphism

  • All states implement the same interface
  • Context can work with any state without knowing its concrete class
  • State-specific behavior is achieved through polymorphism

5. Dependency Inversion

  • Context depends on State abstraction, not concrete states
  • Concrete states also depend on the State interface
  • Reduces coupling between components

Pain Points Without the State Pattern

Without the State pattern, you might encounter these issues:

1. Complex Conditional Logic

Without State pattern:

class VendingMachine:
    def __init__(self):
        self.state = "idle"
        self.has_money = False
        self.inventory = 10
    
    def insert_coin(self):
        if self.state == "idle":
            self.has_money = True
            self.state = "has_money"
            print("Coin inserted")
        elif self.state == "has_money":
            print("Coin already inserted")
        elif self.state == "dispensing":
            print("Please wait")
        elif self.state == "out_of_stock":
            print("Out of stock")
    
    def select_product(self):
        if self.state == "idle":
            print("Insert coins first")
        elif self.state == "has_money":
            if self.inventory > 0:
                self.state = "dispensing"
                self.dispense()
            else:
                self.state = "out_of_stock"
        elif self.state == "dispensing":
            print("Please wait")
        elif self.state == "out_of_stock":
            print("Out of stock")
    
    # More methods with similar if-else chains...

Problems:

  • Every method has complex if-else chains
  • Hard to understand the flow
  • Difficult to add new states
  • Error-prone when modifying state logic

2. Violation of Single Responsibility

  • One class handles all state-specific behavior
  • Becomes a “god class” that knows too much
  • Difficult to test individual state behaviors

3. Difficult to Extend

  • Adding a new state requires modifying multiple methods
  • Risk of breaking existing functionality
  • Can’t add states without changing core class

4. Poor Maintainability

  • State transitions scattered throughout the code
  • Hard to visualize state machine
  • Difficult to ensure all transitions are valid

5. Testing Challenges

  • Hard to test individual state behaviors
  • Need to set up complex conditions to test each state
  • Can’t mock or substitute states easily

When to Use the State Pattern

The State pattern is particularly useful when:

  1. Object behavior depends on its state and must change at runtime
  2. Operations have large conditional statements that depend on object state
  3. State transitions are complex and need to be explicit
  4. You want to avoid duplicate code across similar states
  5. State-specific behavior needs to be easily extensible
  6. You need to make state transitions explicit and manageable

Common Use Cases

  1. Workflow Systems: Document approval, order processing
  2. Network Protocols: TCP connections, HTTP sessions
  3. Game Development: Character states, game phases
  4. UI Components: Button states (enabled, disabled, pressed)
  5. Vending Machines: Product dispensing logic
  6. Media Players: Playing, paused, stopped states
  7. Order Processing: New, paid, shipped, delivered

Advantages

  1. Eliminates Conditional Logic: Replaces complex if-else with polymorphism
  2. Single Responsibility: Each state class has one job
  3. Easy to Extend: New states can be added without changing existing code
  4. Explicit State Transitions: Makes state machine clear and manageable
  5. Improved Testability: Each state can be tested independently

Disadvantages

  1. Increased Number of Classes: Each state requires a separate class
  2. Overkill for Simple Cases: Too complex for simple state machines
  3. State Explosion: Can lead to many classes if states are numerous
  4. Shared State Management: Need to carefully manage shared data

Best Practices

  1. Keep States Focused: Each state should handle only its specific behavior
  2. Use Enums for Status: Provide a way to query current state
  3. Document Transitions: Make state transition diagram clear
  4. Handle Invalid Transitions: Gracefully handle invalid state transitions
  5. Consider State Hierarchy: Use inheritance for related states
  6. Encapsulate Transitions: Let states manage their own transitions

State vs Strategy Pattern

Both patterns use composition and delegation, but they differ:

Aspect State Pattern Strategy Pattern
Purpose Change behavior based on internal state Choose algorithm at runtime
Awareness States know about each other Strategies are independent
Transitions States can trigger transitions Client changes strategy
Context Context behavior changes with state Context behavior stays same
Use Case Object lifecycle, workflows Interchangeable algorithms

Conclusion

The State Design Pattern is a powerful tool for managing complex state-dependent behavior. It helps you eliminate conditional logic, makes state transitions explicit, and creates a more maintainable and extensible codebase.

By encapsulating state-specific behavior into separate classes, you can:

  • Eliminate complex conditional statements
  • Make state transitions explicit and manageable
  • Add new states without modifying existing code
  • Test state-specific behavior independently
  • Create clearer, more maintainable code

Understanding and applying the State pattern will help you write more maintainable, flexible, and robust code, especially in scenarios where object behavior depends heavily on its internal state.