Implementing the State Pattern in C

The State design pattern is a behavioral pattern that allows an object to change its behavior when its internal state changes, appearing as if the object changed its class. This article explains the fundamentals of the State pattern, highlights the architectural problems it solves by replacing complex conditional logic, and provides a clear, practical C# implementation demonstrating how to manage state transitions cleanly and maintainably.

What Is the State Pattern?

In software development, objects frequently need to change how they respond to operations depending on their current condition. A standard procedural approach relies on large switch statements or nested if-else blocks inside every method to check an object's current state. This approach quickly becomes brittle, violates the Open/Closed Principle, and makes adding new states error-prone.

The State pattern solves this by encapsulating state-specific behavior into separate classes. The original object, known as the Context, maintains a reference to an instance of a Concrete State that represents its current condition. When an operation is called on the Context, it delegates the execution to the current state object.

Core Components

  1. Context: Defines the interface of interest to clients and maintains an instance of a Concrete State subclass that defines the current state.
  2. State Interface: Declares methods that every concrete state must implement.
  3. Concrete States: Provide implementations for the behaviors associated with a particular state of the Context.

Implementing the State Pattern in C

Consider an e-commerce Order that progresses through three states: Draft, Paid, and Shipped.

1. Define the State Interface

The interface defines the operations that depend on the state. Each method represents an action that may trigger a state transition.

public interface IOrderState
{
    void AddItem(Order order);
    void ProcessPayment(Order order);
    void Ship(Order order);
}

2. Implement the Context

The Context delegates actions to its internal _state reference and provides a method to update that reference.

public class Order
{
    private IOrderState _state;

    public Order()
    {
        // Initial state
        _state = new DraftOrderState();
    }

    public void TransitionTo(IOrderState state)
    {
        Console.WriteLine($"Order: Transitioning to {state.GetType().Name}.");
        _state = state;
    }

    public void AddItem() => _state.AddItem(this);
    public void ProcessPayment() => _state.ProcessPayment(this);
    public void Ship() => _state.Ship(this);
}

3. Implement Concrete States

Each concrete state defines what happens when an action is executed and dictates the subsequent transition.

public class DraftOrderState : IOrderState
{
    public void AddItem(Order order)
    {
        Console.WriteLine("Item added to order.");
    }

    public void ProcessPayment(Order order)
    {
        Console.WriteLine("Payment processed successfully.");
        order.TransitionTo(new PaidOrderState());
    }

    public void Ship(Order order)
    {
        Console.WriteLine("Cannot ship an order that has not been paid for.");
    }
}

public class PaidOrderState : IOrderState
{
    public void AddItem(Order order)
    {
        Console.WriteLine("Cannot add items to an order that is already paid.");
    }

    public void ProcessPayment(Order order)
    {
        Console.WriteLine("Order has already been paid.");
    }

    public void Ship(Order order)
    {
        Console.WriteLine("Order has been shipped.");
        order.TransitionTo(new ShippedOrderState());
    }
}

public class ShippedOrderState : IOrderState
{
    public void AddItem(Order order)
    {
        Console.WriteLine("Cannot add items to a completed order.");
    }

    public void ProcessPayment(Order order)
    {
        Console.WriteLine("Cannot re-pay for a completed order.");
    }

    public void Ship(Order order)
    {
        Console.WriteLine("Order is already shipped.");
    }
}

Execution and Output

When interacting with the Order object, the caller invokes methods on the context directly, while the underlying behavior and transitions occur automatically:

var order = new Order();

order.AddItem();          // Item added to order.
order.Ship();             // Cannot ship an order that has not been paid for.
order.ProcessPayment();   // Payment processed successfully. Order transitions to PaidOrderState.
order.Ship();             // Order has been shipped. Order transitions to ShippedOrderState.
order.AddItem();          // Cannot add items to a completed order.

Managing State Transitions: Context vs. States

When managing transitions in C#, you have two design options:

  1. State-Driven Transitions (Used Above): Concrete states know which state should come next and trigger order.TransitionTo(new NextState()). This is ideal when the transitions are static and sequential, but it couples states to one another.
  2. Context-Driven Transitions: The states return an outcome or status to the context, and the context decides the next state. This minimizes coupling between concrete states, making individual states reusable across different workflows.

When to Use the State Pattern

  • Use the State pattern when an object's behavior changes dramatically based on its state and the runtime logic contains bloated conditional branches.
  • Use it when different states share common triggers but must handle them in mutually exclusive, complex ways.
  • Avoid the pattern for simple flags or binary state management (such as IsEnabled or IsActive), where basic C# pattern matching or switch expressions are more concise and introduce less overhead.