patron-state

State Pattern: Behavior Based on State

  • 5 min

The State is a pattern that delegates the behavior of a context to an object representing its current state. When the state changes, the same context responds differently without filling all its methods with conditionals.

Imagine a Document in a content management system (CMS).

  1. It starts as a Draft.
  2. It moves to Moderation.
  3. Finally, it becomes Published.

The behavior of the Publish() method changes radically depending on which phase we are in.

  • If it is a Draft, Publish moves the document to Moderation.
  • If it is in Moderation, Publish (if you are an admin) makes it visible to the world.
  • If it is already Published, Publish should do nothing or throw an error.

The Problem: The Monolithic State Machine

The intuitive (and bad) way to solve this is to use a simple variable to store the state and fill the methods with conditionals.

// ❌ The Giant Switch Anti-pattern
class Document
{
    public string State = "Draft";

    public void Publish()
    {
        switch (State)
        {
            case "Draft":
                State = "Moderation";
                break;
            case "Moderation":
                State = "Published"; // Only if admin...
                break;
            case "Published":
                Console.WriteLine("Already published!");
                break;
        }
    }
}
Copied!

This quickly becomes unmanageable. If we add a new state (Archived), we have to review all methods of the Document class and add a new case.

The State pattern suggests that each state should be its own class. The document will delegate the execution of the behavior to the object representing its current state.

The Solution: “I am what my state is”

The main idea is that the original object (Context) maintains a reference to a state object. When asked to do something, the Context says: “I don’t know, ask my current State.”

This allows the object to appear to change its class at runtime.

Implementation in C#

Let’s implement the flow of our Document.

The State Interface

We define the possible actions in our system. Notice that the methods receive the Document (context) to be able to change its state.

// State (Abstract or Interface)
public abstract class DocumentState
{
    // Abstract methods for possible actions
    public abstract void Publish(Document context);
    public abstract void Reject(Document context);
}
Copied!

The Context (Document)

This is the class used by the client.

public class Document
{
    // Current state
    private DocumentState _currentState;

    public Document()
    {
        // Initial state
        _currentState = new DraftState();
    }

    // Method to change state (used by concrete states)
    public void TransitionTo(DocumentState newState)
    {
        Console.WriteLine($"Context: Transition from {_currentState.GetType().Name} to {newState.GetType().Name}");
        _currentState = newState;
    }

    // Document actions: Delegate to the state
    public void Publish() => _currentState.Publish(this);
    public void Reject() => _currentState.Reject(this);
}
Copied!

The Concrete States

Here is the business logic and transition rules. Each class is responsible for its own behavior.

// State 1: Draft
public class DraftState : DocumentState
{
    public override void Publish(Document context)
    {
        Console.WriteLine("Draft -> Sending to moderation...");
        context.TransitionTo(new ModerationState());
    }

    public override void Reject(Document context)
    {
        Console.WriteLine("Draft -> Deleting draft. Goodbye!");
        // Deletion logic...
    }
}

// State 2: Moderation
public class ModerationState : DocumentState
{
    public override void Publish(Document context)
    {
        Console.WriteLine("Moderation -> Approved! Publishing to the web.");
        context.TransitionTo(new PublishedState());
    }

    public override void Reject(Document context)
    {
        Console.WriteLine("Moderation -> Rejected. Back to draft.");
        context.TransitionTo(new DraftState());
    }
}

// State 3: Published
public class PublishedState : DocumentState
{
    public override void Publish(Document context)
    {
        Console.WriteLine("Published -> Do nothing, it's already published.");
    }

    public override void Reject(Document context)
    {
        Console.WriteLine("Published -> Retracting publication. Back to Draft.");
        context.TransitionTo(new DraftState());
    }
}
Copied!

Execution

class Program
{
    static void Main(string[] args)
    {
        var doc = new Document();

        // We are in Draft
        doc.Publish(); // Moves to Moderation

        // We are in Moderation
        doc.Publish(); // Moves to Published

        // We are in Published
        doc.Publish(); // Does nothing

        // We retract the document
        doc.Reject(); // Back to Draft
    }
}
Copied!

Output:

Draft -> Sending to moderation...
Context: Transition from DraftState to ModerationState
Moderation -> Approved! Publishing to the web.
Context: Transition from ModerationState to PublishedState
Published -> Do nothing, it's already published.
Published -> Retracting publication. Back to Draft.
Context: Transition from PublishedState to DraftState
Copied!

State vs Strategy

This is the million-dollar question, because the UML diagrams are almost identical.

FeatureStrategyState
Origin of ChangeThe Client decides to change the strategy (“I want walking route now”).The State itself or the Context decides the change (“I finished the draft, moving to moderation”).
KnowledgeStrategies are usually independent of each other.States often know about each other (one state creates the next).
PurposeSwap algorithms.Model the lifecycle or phases of an object.

When to use State?

  1. State Machines: When you have an object that behaves like a finite state machine (FSM).
  2. Complex Conditionals: When you have giant methods full of switch statements that depend on the object’s state.
  3. Runtime Changes: When the behavior of an object must change drastically depending on its internal fields.

The State pattern cleans our code by distributing the logic of each phase into its own class. It greatly facilitates adding new states without breaking existing ones (Open/Closed Principle).