Understanding the State Pattern
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 as its behavior changes based on the state it's in.
Problem
Imagine you're developing a document editing application with a document that can be in different states: Draft, Moderation, Published. Each state determines what operations can be performed on the document. For example:
- When a document is in a Draft state, it can be edited and submitted for review.
- In Moderation, it can be either approved or rejected.
- Once Published, it can be viewed by anyone but can't be edited without making a new draft.
Implementing this with conditional statements (like if/else or switch) can lead to several problems:
- Code becomes difficult to maintain as you add more states and state-dependent behaviors
- State transition logic gets scattered throughout the code
- Adding new states requires modifying multiple conditional blocks
- The code becomes prone to errors when logic isn't updated consistently
Solution
The State pattern suggests creating new classes for all possible states of an object and extracting state-specific behaviors into these classes. The original object (called Context) stores a reference to one of the state objects that represents its current state and delegates all state-specific work to that object.
When the context's state changes, it simply switches to another state object. This way:
- State-specific code is isolated in separate classes
- State transitions become explicit
- Each state is self-contained and only concerned with its own behavior
- New states can be added without modifying existing state code
Structure
Participants
- Context: Defines the interface of interest to clients and maintains a reference to an instance of a State subclass, which represents the current state.
- State: The interface for all concrete states, encapsulating the state-specific behaviors.
- Concrete States: Each subclass implements behavior associated with a particular state of the Context.
When to Use
Use the State Pattern when:
- An object's behavior depends on its state, and it must change its behavior at runtime based on that state
- Operations have large, multipart conditional statements that depend on the object's state
- The transitions between states follow a predictable pattern and should be explicitly represented
- You want to avoid the proliferation of state-specific code across multiple methods
Benefits
- Single Responsibility Principle: Organizes the code related to particular states into separate classes.
- Open/Closed Principle: Allows introducing new states without changing existing state classes or the context.
- Simplified Code: Eliminates bulky state machine conditionals from the context code.
- Explicit Transitions: Makes state transitions explicit and easier to understand.
Real-World Uses
- Document Processing: Managing document states in workflow systems (Draft, Review, Published, etc.)
- Order Processing: Managing orders in e-commerce systems (New, Paid, Shipped, Delivered, etc.)
- Game Development: Managing character states (Standing, Walking, Jumping, etc.)
- Network Connections: Managing connection states (Connecting, Connected, Disconnected)
- UI Elements: Managing the state of buttons, windows, and other UI elements
Implementation Example
Here's a JavaScript implementation of the State pattern for a document workflow system:
// Context class
class Document {
constructor() {
// Initialize with Draft state
this.state = new DraftState(this);
this.content = "";
}
// Methods that delegate to the current state
edit(content) {
this.state.edit(content);
}
review() {
this.state.review();
}
approve() {
this.state.approve();
}
reject() {
this.state.reject();
}
publish() {
this.state.publish();
}
// Method to change the document's state
changeState(state) {
this.state = state;
}
// Business logic methods that can be called by states
setContent(content) {
this.content = content;
}
getContent() {
return this.content;
}
}
// State interface (abstract class in JS)
class DocumentState {
constructor(document) {
this.document = document;
}
edit(content) {
console.log("Operation not allowed in current state");
}
review() {
console.log("Operation not allowed in current state");
}
approve() {
console.log("Operation not allowed in current state");
}
reject() {
console.log("Operation not allowed in current state");
}
publish() {
console.log("Operation not allowed in current state");
}
}
// Concrete State: Draft
class DraftState extends DocumentState {
constructor(document) {
super(document);
}
edit(content) {
this.document.setContent(content);
console.log("Document updated with new content");
}
review() {
console.log("Document submitted for review");
this.document.changeState(new ModerationState(this.document));
}
}
// Concrete State: Moderation
class ModerationState extends DocumentState {
constructor(document) {
super(document);
}
approve() {
console.log("Document has been approved");
this.document.changeState(new PublishedState(this.document));
}
reject() {
console.log("Document has been rejected, returning to draft");
this.document.changeState(new DraftState(this.document));
}
}
// Concrete State: Published
class PublishedState extends DocumentState {
constructor(document) {
super(document);
}
edit(content) {
console.log("Creating a new draft from published document");
// First, transition back to draft
this.document.changeState(new DraftState(this.document));
// Then edit the content
this.document.edit(content);
}
}
// Client code
function clientCode() {
const document = new Document();
// Initial draft
document.edit("Initial draft content");
// Submit for review
document.review();
// Try to edit while in moderation (not allowed)
document.edit("Trying to edit in moderation");
// Approve the document
document.approve();
// Try to approve again (not allowed in published state)
document.approve();
// Edit the published document (creates a new draft)
document.edit("Updated content for new draft");
// Submit for review again
document.review();
// Reject this time
document.reject();
// Now we're back in draft, can edit again
document.edit("Improved content after rejection");
}
clientCode();
In this example:
- The
Document
class is the context that maintains a reference to the current state. - The
DocumentState
defines the interface for all states and provides default implementations for all operations. - Concrete state classes (
DraftState
,ModerationState
,PublishedState
) implement the specific behaviors for each state. - State transitions are handled within the state classes themselves, making them explicit and centralized.
- The client code interacts only with the
Document
class, which delegates the operations to the current state.
Interactive Demo
Experience the State pattern in action with this interactive demo of a document workflow system. See how the document's behavior changes as it transitions between different states.
Interactive Demo: Document Workflow
Use the controls below to transition the document through various states and see how its behavior changes.