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
- Context: Defines the interface of interest to clients and maintains an instance of a Concrete State subclass that defines the current state.
- State Interface: Declares methods that every concrete state must implement.
- 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:
- 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. - 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
IsEnabledorIsActive), where basic C# pattern matching orswitchexpressions are more concise and introduce less overhead.