The State Pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. The pattern encapsulates state-specific behavior into separate state classes and delegates behavior to the current state object, making it appear as if the object changed its class.
Core Concept
Instead of using complex conditional statements to handle different behaviors based on an object’s state, the State Pattern extracts each state into a separate class that implements a common interface. The context object maintains a reference to one of these state objects and delegates all state-dependent operations to it.
Structure
┌─────────────┐ uses ┌─────────────┐
│ Context │◆──────────▶│ State │
├─────────────┤ ├─────────────┤
│- state │ │+ handle() │
│+ request() │ └─────────────┘
│+ setState() │ △
└─────────────┘ │
│
┌──────────────┼──────────────┐
│ │ │
┌───────▼──────┐ ┌───────▼──────┐ ┌───────▼──────┐
│ConcreteStateA│ │ConcreteStateB│ │ConcreteStateC│
├──────────────┤ ├──────────────┤ ├──────────────┤
│+ handle() │ │+ handle() │ │+ handle() │
└──────────────┘ └──────────────┘ └──────────────┘
Key Components
State Interface/Abstract Class: Defines the interface for encapsulating behavior associated with a particular state.
Concrete States: Implement behavior associated with a specific state of the context.
Context: Maintains a reference to a state object and delegates state-specific requests to this object.
Benefits
- Eliminates complex conditionals: Removes lengthy if-else or switch statements
- Localizes state-specific behavior: Each state’s behavior is contained in its own class
- Makes state transitions explicit: State changes are clearly defined operations
- Supports Open/Closed Principle: New states can be added without modifying existing code
- Improves maintainability: Changes to one state don’t affect others
When to Use
- An object’s behavior depends on its state and must change at runtime
- Operations have large, multipart conditional statements that depend on object state
- State-specific behavior is scattered across multiple methods
- You need to avoid duplicate code when similar states share common behavior
Implementation Considerations
State Transitions: States can trigger their own transitions or leave transition control to the context.
State Storage: The context typically stores the current state, but states can also be stored as singletons if they’re stateless.
State Creation: States can be created on-demand or pre-instantiated depending on performance and memory requirements.
Example 1: Media Player
This example demonstrates a media player that behaves differently based on its current state. The MediaPlayer
context delegates all operations to the current state object. Each state (PlayingState
, PausedState
, StoppedState
) implements different behaviors for the same operations.
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from __future__ import annotations
class MediaPlayerState(ABC):
"""Abstract base class for media player states."""
@abstractmethod
def play(self, player: 'MediaPlayer') -> None:
"""Handle play action for this state."""
pass
@abstractmethod
def pause(self, player: 'MediaPlayer') -> None:
"""Handle pause action for this state."""
pass
@abstractmethod
def stop(self, player: 'MediaPlayer') -> None:
"""Handle stop action for this state."""
pass
@abstractmethod
def get_status(self) -> str:
"""Return current state status."""
pass
class PlayingState(MediaPlayerState):
"""Concrete state representing the playing state."""
def play(self, player: 'MediaPlayer') -> None:
print("Already playing. Media continues...")
def pause(self, player: 'MediaPlayer') -> None:
print("Pausing playback...")
player.set_state(PausedState())
def stop(self, player: 'MediaPlayer') -> None:
print("Stopping playback...")
player.set_state(StoppedState())
def get_status(self) -> str:
return "Playing"
class PausedState(MediaPlayerState):
"""Concrete state representing the paused state."""
def play(self, player: 'MediaPlayer') -> None:
print("Resuming playback...")
player.set_state(PlayingState())
def pause(self, player: 'MediaPlayer') -> None:
print("Already paused.")
def stop(self, player: 'MediaPlayer') -> None:
print("Stopping from pause...")
player.set_state(StoppedState())
def get_status(self) -> str:
return "Paused"
class StoppedState(MediaPlayerState):
"""Concrete state representing the stopped state."""
def play(self, player: 'MediaPlayer') -> None:
print("Starting playback...")
player.set_state(PlayingState())
def pause(self, player: 'MediaPlayer') -> None:
print("Cannot pause. Media is stopped. Press play first.")
def stop(self, player: 'MediaPlayer') -> None:
print("Already stopped.")
def get_status(self) -> str:
return "Stopped"
class MediaPlayer:
"""Context class that uses state objects to handle behavior."""
def __init__(self) -> None:
# Initialize with stopped state
self._state: MediaPlayerState = StoppedState()
self._current_track: str = "No track loaded"
def set_state(self, state: MediaPlayerState) -> None:
"""Change the current state."""
self._state = state
def play(self) -> None:
"""Delegate play action to current state."""
self._state.play(self)
def pause(self) -> None:
"""Delegate pause action to current state."""
self._state.pause(self)
def stop(self) -> None:
"""Delegate stop action to current state."""
self._state.stop(self)
def load_track(self, track_name: str) -> None:
"""Load a new track (available in any state)."""
self._current_track = track_name
print(f"Loaded track: {track_name}")
def get_status(self) -> str:
"""Get current player status."""
return f"Status: {self._state.get_status()} | Track: {self._current_track}"
# Demonstration
if __name__ == "__main__":
player = MediaPlayer()
print("=== Media Player State Pattern Demo ===")
print(player.get_status())
print()
# Try to pause when stopped
player.pause()
print()
# Load and play a track
player.load_track("Bohemian Rhapsody")
player.play()
print(player.get_status())
print()
# Pause the track
player.pause()
print(player.get_status())
print()
# Resume playing
player.play()
print(player.get_status())
print()
# Stop the player
player.stop()
print(player.get_status())
print()
# Try to play when stopped
player.play()
print(player.get_status())
Key Points:
- State transitions are handled within the state classes themselves
- Each state knows which transitions are valid from its current position
- The context remains simple and focused on delegation
- Adding new states (like “Loading” or “Buffering”) requires no changes to existing code
Example 2: Document Workflow
This example shows a document workflow system where documents progress through different states: Draft → Under Review → Approved → Published. Each state enforces different rules about what operations are allowed.
from abc import ABC, abstractmethod
from datetime import datetime
from typing import TYPE_CHECKING, List, Optional
if TYPE_CHECKING:
from __future__ import annotations
class DocumentState(ABC):
"""Abstract base class for document workflow states."""
@abstractmethod
def edit(self, document: 'Document', content: str) -> bool:
"""Attempt to edit the document."""
pass
@abstractmethod
def submit_for_review(self, document: 'Document') -> bool:
"""Attempt to submit document for review."""
pass
@abstractmethod
def approve(self, document: 'Document', reviewer: str) -> bool:
"""Attempt to approve the document."""
pass
@abstractmethod
def reject(self, document: 'Document', reason: str) -> bool:
"""Attempt to reject the document."""
pass
@abstractmethod
def publish(self, document: 'Document') -> bool:
"""Attempt to publish the document."""
pass
@abstractmethod
def get_state_name(self) -> str:
"""Return the name of this state."""
pass
class DraftState(DocumentState):
"""Document is in draft mode - can be edited freely."""
def edit(self, document: 'Document', content: str) -> bool:
document._content = content
document._last_modified = datetime.now()
document._add_log("Document edited in draft state")
return True
def submit_for_review(self, document: 'Document') -> bool:
if not document._content.strip():
document._add_log("Cannot submit empty document for review")
return False
document._add_log("Document submitted for review")
document._set_state(UnderReviewState())
return True
def approve(self, document: 'Document', reviewer: str) -> bool:
document._add_log("Cannot approve document in draft state")
return False
def reject(self, document: 'Document', reason: str) -> bool:
document._add_log("Cannot reject document in draft state")
return False
def publish(self, document: 'Document') -> bool:
document._add_log("Cannot publish document directly from draft")
return False
def get_state_name(self) -> str:
return "Draft"
class UnderReviewState(DocumentState):
"""Document is under review - editing restricted."""
def edit(self, document: 'Document', content: str) -> bool:
document._add_log("Cannot edit document while under review")
return False
def submit_for_review(self, document: 'Document') -> bool:
document._add_log("Document is already under review")
return False
def approve(self, document: 'Document', reviewer: str) -> bool:
document._add_log(f"Document approved by {reviewer}")
document._approved_by = reviewer
document._approved_date = datetime.now()
document._set_state(ApprovedState())
return True
def reject(self, document: 'Document', reason: str) -> bool:
document._add_log(f"Document rejected: {reason}")
document._rejection_reason = reason
document._set_state(DraftState()) # Back to draft for editing
return True
def publish(self, document: 'Document') -> bool:
document._add_log("Cannot publish document that hasn't been approved")
return False
def get_state_name(self) -> str:
return "Under Review"
class ApprovedState(DocumentState):
"""Document has been approved - ready for publishing."""
def edit(self, document: 'Document', content: str) -> bool:
document._add_log("Cannot edit approved document without returning to draft")
return False
def submit_for_review(self, document: 'Document') -> bool:
document._add_log("Document is already approved")
return False
def approve(self, document: 'Document', reviewer: str) -> bool:
document._add_log("Document is already approved")
return False
def reject(self, document: 'Document', reason: str) -> bool:
document._add_log(f"Approved document rejected: {reason}")
document._rejection_reason = reason
document._set_state(DraftState())
return True
def publish(self, document: 'Document') -> bool:
document._add_log("Document published")
document._published_date = datetime.now()
document._set_state(PublishedState())
return True
def get_state_name(self) -> str:
return "Approved"
class PublishedState(DocumentState):
"""Document has been published - read-only."""
def edit(self, document: 'Document', content: str) -> bool:
document._add_log("Cannot edit published document")
return False
def submit_for_review(self, document: 'Document') -> bool:
document._add_log("Published document cannot be submitted for review")
return False
def approve(self, document: 'Document', reviewer: str) -> bool:
document._add_log("Published document cannot be approved")
return False
def reject(self, document: 'Document', reason: str) -> bool:
document._add_log("Published document cannot be rejected")
return False
def publish(self, document: 'Document') -> bool:
document._add_log("Document is already published")
return False
def get_state_name(self) -> str:
return "Published"
class Document:
"""Context class representing a document in a workflow system."""
def __init__(self, title: str, author: str) -> None:
self._title: str = title
self._author: str = author
self._content: str = ""
self._state: DocumentState = DraftState()
self._created_date: datetime = datetime.now()
self._last_modified: datetime = datetime.now()
self._log: List[str] = []
self._approved_by: Optional[str] = None
self._approved_date: Optional[datetime] = None
self._published_date: Optional[datetime] = None
self._rejection_reason: Optional[str] = None
self._add_log(f"Document '{title}' created by {author}")
def _set_state(self, state: DocumentState) -> None:
"""Change the document's state."""
self._state = state
def _add_log(self, message: str) -> None:
"""Add an entry to the document's activity log."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self._log.append(f"[{timestamp}] {message}")
# Public interface methods that delegate to the current state
def edit(self, content: str) -> bool:
"""Edit the document content."""
return self._state.edit(self, content)
def submit_for_review(self) -> bool:
"""Submit the document for review."""
return self._state.submit_for_review(self)
def approve(self, reviewer: str) -> bool:
"""Approve the document."""
return self._state.approve(self, reviewer)
def reject(self, reason: str) -> bool:
"""Reject the document."""
return self._state.reject(self, reason)
def publish(self) -> bool:
"""Publish the document."""
return self._state.publish(self)
def get_status(self) -> str:
"""Get current document status."""
return (f"Title: {self._title} | Author: {self._author} | "
f"State: {self._state.get_state_name()} | "
f"Content Length: {len(self._content)} chars")
def get_activity_log(self) -> List[str]:
"""Get the document's activity log."""
return self._log.copy()
# Demonstration
if __name__ == "__main__":
print("=== Document Workflow State Pattern Demo ===")
# Create a new document
doc = Document("API Documentation", "Alice Smith")
print(f"Document created: {doc.get_status()}")
print()
# Edit the document (allowed in draft state)
doc.edit("This is the initial content of the API documentation...")
print("After editing:", doc.get_status())
print()
# Try to publish directly (should fail)
print("Attempting to publish directly:")
doc.publish()
print()
# Submit for review
print("Submitting for review:")
doc.submit_for_review()
print(doc.get_status())
print()
# Try to edit while under review (should fail)
print("Attempting to edit while under review:")
doc.edit("Trying to modify content...")
print()
# Approve the document
print("Approving document:")
doc.approve("Bob Johnson")
print(doc.get_status())
print()
# Publish the approved document
print("Publishing document:")
doc.publish()
print(doc.get_status())
print()
# Try to edit published document (should fail)
print("Attempting to edit published document:")
doc.edit("Cannot change this!")
print()
# Show activity log
print("=== Document Activity Log ===")
for log_entry in doc.get_activity_log():
print(log_entry)
Key Points:
- Each state encapsulates the business rules for that particular workflow stage
- State transitions follow a defined workflow path
- The document context maintains all data while states control behavior
- Invalid operations are gracefully handled with appropriate feedback
- The system maintains an audit log of all state changes and operations
The State Pattern provides a clean, maintainable solution for complex state-dependent behavior while keeping the code organized and extensible.