Skip to content

State Pattern

Overview

The State 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 state classes and delegates state-dependent operations to the current state object.

Intent

  • Allow an object to alter its behavior when its internal state changes
  • Encapsulate state-specific behavior in separate state objects
  • Make state transitions explicit and eliminate large conditional statements
  • Localize state-specific behavior and partition behavior for different states

Problem

When an object's behavior depends on its state, and it must change its behavior at runtime depending on that state, you often end up with large conditional statements (if-else or switch-case) that check the state and execute different code. This approach becomes difficult to maintain as:

  • Adding new states requires modifying existing code
  • State-dependent behavior is scattered throughout the class
  • State transitions are implicit and hard to understand
  • The code violates the Open/Closed Principle

Solution

Define separate state classes that encapsulate state-specific behavior. The context maintains a reference to one of the concrete state objects and delegates all state-dependent operations to it. When the state changes, the context switches to a different state object.

Structure

Class Diagram

State Pattern Diagram 0

Components

  1. Context

    • Maintains an instance of a concrete state that defines the current state
    • Delegates state-specific requests to the current state object
    • May pass itself as an argument to the state object
  2. State (Interface)

    • Defines an interface for encapsulating behavior associated with a particular state
    • Declares methods that all concrete states must implement
  3. ConcreteState

    • Implements behavior associated with a specific state of the context
    • Can trigger state transitions by calling context's setState()
    • Each concrete state knows which state should follow it

Implementation Examples

Example 1: TCP Connection State

Network connections go through different states (Closed, Listen, Established).

State Pattern Diagram 1

State Transitions:

State Transitions Diagram

Usage:

// Create connection in closed state
TCPConnection connection(std::make_shared<TCPClosed>());

// Open connection (Closed -> Listen)
connection.open();

// Receive acknowledgment (Listen -> Established)
connection.acknowledge();

// Send data
connection.acknowledge();  // Data acknowledged

// Close connection (Established -> Closed)
connection.close();

Example 2: Vending Machine

A vending machine has different states based on coin insertion and product availability.

State Pattern Diagram 2

Usage:

// Create vending machine with 5 products
VendingMachine machine(std::make_shared<NoCoinState>(), 5);

// Insert coins (NoCoin -> HasCoin)
machine.insertCoin();
machine.insertCoin();  // Enough coins

// Select product (HasCoin -> Dispensing)
machine.selectProduct();
// Automatically dispenses (Dispensing -> NoCoin)

// Eject coins scenario
machine.insertCoin();
machine.insertCoin();
machine.ejectCoin();  // Coins returned (HasCoin -> NoCoin)

Example 3: Document Workflow

Documents go through different stages: Draft, Moderation, Published.

State Pattern Diagram 3

Usage:

// Create document in draft state
Document doc("John Doe", "Article Content", 
             std::make_shared<DraftState>());

// Submit for moderation (Draft -> Moderation)
doc.publish();

// Review document
doc.review();

// Approve and publish (Moderation -> Published)
doc.approve();

// Rejection scenario
Document doc2("Jane", "Content", std::make_shared<DraftState>());
doc2.publish();           // Draft -> Moderation
doc2.reject();            // Moderation -> Draft
doc2.publish();           // Draft -> Moderation (again)
doc2.approve();           // Moderation -> Published

Example 4: Media Player

Media players have different playback states: Stopped, Playing, Paused.

State Pattern Diagram 4

State Behavior:

Operation Stopped Playing Paused
play() Start playback (→ Playing) Already playing Resume playback (→ Playing)
pause() Cannot pause Pause playback (→ Paused) Already paused
stop() Already stopped Stop playback (→ Stopped) Stop playback (→ Stopped)

Usage:

1
2
3
4
5
6
MediaPlayer player(std::make_shared<StoppedState>(), "song.mp3");

player.play();   // Stopped -> Playing
player.pause();  // Playing -> Paused
player.play();   // Paused -> Playing
player.stop();   // Playing -> Stopped

Sequence Diagram

State Pattern Diagram 5

Real-World Applications

1. Network Protocols

TCP/IP Connection States:

TCP/IP Connection States

Each state handles packets differently: - LISTEN: Accepts connection requests - ESTABLISHED: Transfers data - FIN_WAIT: Handles connection termination

HTTP Request Handling:

HTTP Request Handling

2. User Interface

Form Validation:

Form Validation States

Button States: - Enabled: Responds to clicks - Disabled: Ignores input - Pressed: Visual feedback - Loading: Shows spinner

Dialog States:

Dialog States

3. Workflow Systems

Order Processing:

Order Processing States

Issue Tracking:

Issue Tracking States

Document Approval:

Document Approval States

4. Game Development

Character States:

1
2
3
4
class Character {
    std::shared_ptr<CharacterState> state_;
    // Idle, Walking, Running, Jumping, Attacking, Dead
};

States handle: - Input differently (can't jump while attacking) - Animation (different sprites per state) - Transitions (can run from walking, can't walk while jumping)

AI Behavior:

AI Behavior States

Game Flow:

Game Flow States

5. Media Applications

Video Player: - Stopped: Show thumbnail - Buffering: Show loading indicator - Playing: Update progress bar - Paused: Show play button - Error: Show error message

Audio Recording:

Audio Recording States

6. Transaction Systems

ATM Transaction:

ATM Transaction States

Payment Processing:

Payment Processing States

7. Thread and Process States

Thread Lifecycle:

Thread Lifecycle States

Connection Pool:

Connection Pool States

Design Considerations

Advantages

Eliminates Conditional Statements: Replaces large if-else or switch statements with polymorphism.

// Without State Pattern
void handle(Event event) {
    if (state == CLOSED) {
        if (event == OPEN) state = LISTENING;
    } else if (state == LISTENING) {
        if (event == ACK) state = ESTABLISHED;
        else if (event == CLOSE) state = CLOSED;
    } else if (state == ESTABLISHED) {
        // ... more conditions
    }
}

// With State Pattern
void handle(Event event) {
    currentState->handle(event, this);
}

Explicit State Transitions: State transitions are explicit and centralized in state classes.

Single Responsibility Principle: Each state class encapsulates behavior for one state only.

Open/Closed Principle: You can add new states without modifying existing state classes or the context.

Improved Maintainability: State-specific code is isolated and easier to understand and modify.

Testability: Each state can be tested independently.

Disadvantages

Increased Number of Classes: Each state requires a separate class, increasing code volume.

Overkill for Simple Cases: For simple state machines with 2-3 states, a simple flag might suffice.

State Knowledge: States may need to know about other states, creating dependencies.

Context-State Coupling: States often need access to context methods, creating coupling.

State vs Strategy Pattern

Aspect State Pattern Strategy Pattern
Intent Change behavior based on internal state Select algorithm from family of algorithms
Who decides Context or states themselves Client or context
Transitions States can transition to other states Strategies are independent
Context awareness States often know about context Strategies may not know context
Use case Object behavior changes with state Different ways to achieve same goal

State Pattern:

context.request();  // Behavior depends on current state
// State A might transition to State B internally

Strategy Pattern:

1
2
3
4
context.setStrategy(new ConcreteStrategyA());
context.execute();  // Uses strategy A
context.setStrategy(new ConcreteStrategyB());
context.execute();  // Uses strategy B (client decides)

When to Use State Pattern

Use State Pattern when:

  • An object's behavior depends on its state and must change at runtime
  • Operations have large, multipart conditional statements that depend on the object's state
  • State transitions are complex and need to be explicit
  • State-specific behavior is scattered across multiple operations
  • You want to avoid code duplication for state-specific behavior

Example Scenarios: - Network connection management - Game character behavior - UI component states - Workflow systems - Media player controls - Transaction processing

Avoid State Pattern when:

  • You have only 2-3 simple states
  • State changes are rare
  • Simple boolean flags suffice
  • State-specific behavior is minimal
  • The overhead of multiple classes outweighs benefits

Use simpler alternatives: - Boolean flags for binary states - Enum + switch for simple state machines - State machines libraries for complex but well-defined FSMs

Implementation Considerations

1. Who Defines State Transitions?

Option A: States define transitions (more flexible)

1
2
3
4
5
6
class ConcreteStateA : public State {
    void handle(Context* ctx) override {
        // Perform action
        ctx->setState(std::make_shared<ConcreteStateB>());
    }
};

Option B: Context defines transitions (centralized)

1
2
3
4
5
6
7
8
class Context {
    void request() {
        state_->handle();
        if (shouldTransition()) {
            state_ = nextState();
        }
    }
};

2. State Object Creation

Option A: Create on demand (memory efficient)

ctx->setState(std::make_shared<ConcreteStateA>());

Option B: Flyweight pattern (performance efficient)

static std::shared_ptr<StateA> instance_ = std::make_shared<StateA>();
ctx->setState(instance_);

3. State Entry/Exit Actions

class State {
    virtual void onEnter(Context* ctx) {}
    virtual void onExit(Context* ctx) {}
    virtual void handle(Context* ctx) = 0;
};

class Context {
    void setState(std::shared_ptr<State> newState) {
        if (state_) state_->onExit(this);
        state_ = newState;
        if (state_) state_->onEnter(this);
    }
};

Best Practices

1. Use Smart Pointers

1
2
3
class Context {
    std::shared_ptr<State> state_;  // Use shared_ptr for automatic memory management
};

2. Define Clear State Interfaces

1
2
3
4
5
6
class State {
public:
    virtual ~State() = default;
    virtual void handle(Context* ctx) = 0;
    virtual std::string getName() const = 0;  // For debugging
};

3. Validate State Transitions

1
2
3
4
5
6
7
8
class Context {
    void setState(std::shared_ptr<State> newState) {
        if (!isValidTransition(state_, newState)) {
            throw std::runtime_error("Invalid state transition");
        }
        state_ = newState;
    }
};

4. Log State Transitions

1
2
3
4
5
void Context::setState(std::shared_ptr<State> newState) {
    std::cout << "Transitioning from " << state_->getName()
              << " to " << newState->getName() << std::endl;
    state_ = newState;
}

5. Consider State Hierarchies

For complex state machines with substates:

1
2
3
4
5
6
7
class State {
    virtual std::shared_ptr<State> getParentState() { return nullptr; }
};

class PlayingState : public State { /* ... */ };
class FastForwardState : public PlayingState { /* substate */ };
class RewindState : public PlayingState { /* substate */ };
  • Strategy: Similar structure but different intent. Strategy focuses on algorithm selection, State on behavior change based on state.
  • Flyweight: Can be used to share state objects if they have no instance-specific data.
  • Singleton: State objects are often implemented as singletons if they have no state-specific data.
  • Bridge: Similar structure but Bridge separates interface from implementation, while State is about behavior change.

Summary

The State Pattern provides an elegant solution for implementing state machines by: - Eliminating complex conditionals with polymorphic state objects - Making state transitions explicit and easy to understand - Localizing state-specific behavior in separate classes - Supporting the Open/Closed Principle for adding new states

Use it when your object's behavior significantly depends on its state and state transitions are complex. Avoid it for simple state machines where boolean flags or enums would suffice. The pattern shines in scenarios like network protocols, UI components, workflow systems, and game development where state-dependent behavior is complex and needs to be maintainable.