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).
- It starts as a
Draft. - It moves to
Moderation. - Finally, it becomes
Published.
The behavior of the Publish() method changes radically depending on which phase we are in.
- If it is a
Draft,Publishmoves the document toModeration. - If it is in
Moderation,Publish(if you are an admin) makes it visible to the world. - If it is already
Published,Publishshould 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;
}
}
}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);
}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);
}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());
}
}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
}
}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 DraftStateState vs Strategy
This is the million-dollar question, because the UML diagrams are almost identical.
| Feature | Strategy | State |
|---|---|---|
| Origin of Change | The 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”). |
| Knowledge | Strategies are usually independent of each other. | States often know about each other (one state creates the next). |
| Purpose | Swap algorithms. | Model the lifecycle or phases of an object. |
When to use State?
- State Machines: When you have an object that behaves like a finite state machine (FSM).
- Complex Conditionals: When you have giant methods full of
switchstatements that depend on the object’s state. - 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).