Overview
The State Pattern encapsulates state-specific behavior within separate state objects and delegates requests to the current state object. The pattern emerged from the Gang of Four design patterns catalog as a solution to the problem of managing complex conditional logic based on object state. Rather than using large conditional statements that check the current state and execute different code paths, the State Pattern distributes state-specific behavior across multiple state classes.
The pattern operates through three primary components: a context object that maintains a reference to a state object, an abstract state interface that defines common operations, and concrete state classes that implement state-specific behavior. When the context receives a request, it delegates the request to its current state object. The state object processes the request and may trigger a state transition by changing the context's state reference to a different state object.
This approach transforms complex conditional logic into a maintainable object structure. Instead of modifying existing code when adding new states, developers create new state classes. The pattern proves particularly valuable in systems where objects exhibit significantly different behavior across multiple states and where state transitions follow defined rules.
# Simple state pattern structure
class Context
attr_accessor :state
def initialize
@state = ConcreteStateA.new
end
def request
state.handle(self)
end
end
class ConcreteStateA
def handle(context)
puts "Handling in State A"
context.state = ConcreteStateB.new
end
end
class ConcreteStateB
def handle(context)
puts "Handling in State B"
context.state = ConcreteStateA.new
end
end
context = Context.new
context.request # => "Handling in State A"
context.request # => "Handling in State B"
Key Principles
The State Pattern builds on the principle of encapsulating what varies. State-specific behavior varies based on the current state, so the pattern encapsulates each state's behavior in a separate class. This encapsulation prevents state-specific code from scattering throughout the application and concentrates related behavior in single locations.
State transitions represent a core principle of the pattern. States control their own transitions, determining when and how the context transitions to other states. The context delegates decision-making to state objects rather than managing transitions through central logic. This distribution of transition logic prevents the context from accumulating complex state management code.
The pattern follows the Open/Closed Principle by allowing state addition without modifying existing state classes or the context. Each state class operates independently, implementing the same interface but providing different behavior. When requirements change to add a new state, developers create a new state class without touching existing code.
Polymorphism drives the pattern's operation. The context interacts with state objects through a common interface, treating all states uniformly. Runtime polymorphism allows the context to execute different behavior by delegating to different state objects. The context requires no knowledge of concrete state classes, depending only on the abstract state interface.
State objects may be stateless or stateful. Stateless state objects contain only behavior logic and can be shared across multiple contexts, often implemented as singletons. Stateful state objects maintain state-specific data and require separate instances for each context. The choice between these approaches depends on whether states need to maintain their own data beyond the behavior they implement.
The pattern creates a bidirectional relationship between contexts and states. Contexts maintain references to state objects, while state objects often need references to contexts to trigger state transitions. This coupling between contexts and states represents an inherent characteristic of the pattern rather than a design flaw.
Ruby Implementation
Ruby's dynamic nature and flexible object model provide multiple approaches to implementing the State Pattern. The classical implementation uses separate state classes, while Ruby-specific techniques include modules, procs, and method manipulation.
The class-based implementation creates a state hierarchy with a common interface:
class Connection
attr_accessor :state
def initialize
@state = ClosedState.new
end
def open
state.open(self)
end
def close
state.close(self)
end
def read
state.read(self)
end
def write(data)
state.write(self, data)
end
end
class ClosedState
def open(connection)
puts "Opening connection"
connection.state = OpenState.new
end
def close(connection)
puts "Connection already closed"
end
def read(connection)
raise "Cannot read from closed connection"
end
def write(connection, data)
raise "Cannot write to closed connection"
end
end
class OpenState
def open(connection)
puts "Connection already open"
end
def close(connection)
puts "Closing connection"
connection.state = ClosedState.new
end
def read(connection)
puts "Reading data"
end
def write(connection, data)
puts "Writing: #{data}"
end
end
Ruby modules offer an alternative implementation using mixins:
module StatePattern
def state=(new_state)
extend(new_state)
@state_name = new_state
end
end
module DraftState
def publish
puts "Publishing document"
self.state = PublishedState
end
def archive
puts "Archiving draft"
self.state = ArchivedState
end
end
module PublishedState
def publish
puts "Already published"
end
def archive
puts "Archiving published document"
self.state = ArchivedState
end
def unpublish
puts "Unpublishing document"
self.state = DraftState
end
end
module ArchivedState
def restore
puts "Restoring from archive"
self.state = DraftState
end
end
class Document
include StatePattern
def initialize
self.state = DraftState
end
end
doc = Document.new
doc.publish # => "Publishing document"
doc.unpublish # => "Unpublishing document"
Ruby's method manipulation capabilities enable dynamic state implementation:
class Account
attr_reader :balance
def initialize(initial_balance)
@balance = initial_balance
set_state
end
def deposit(amount)
@balance += amount
set_state
end
def withdraw(amount)
@balance -= amount
set_state
end
private
def set_state
case
when @balance < 0
apply_overdrawn_behavior
when @balance < 100
apply_low_balance_behavior
else
apply_normal_behavior
end
end
def apply_normal_behavior
def self.status; "Normal"; end
def self.fees; 0; end
def self.interest_rate; 0.02; end
end
def apply_low_balance_behavior
def self.status; "Low Balance"; end
def self.fees; 5; end
def self.interest_rate; 0.01; end
end
def apply_overdrawn_behavior
def self.status; "Overdrawn"; end
def self.fees; 25; end
def self.interest_rate; 0; end
end
end
State objects implemented as singletons reduce memory overhead when states contain no instance-specific data:
class TrafficLight
attr_accessor :state
def initialize
@state = RedState.instance
end
def next_state
state.next_state(self)
end
def color
state.color
end
end
require 'singleton'
class RedState
include Singleton
def next_state(light)
light.state = GreenState.instance
end
def color
"Red"
end
end
class GreenState
include Singleton
def next_state(light)
light.state = YellowState.instance
end
def color
"Green"
end
end
class YellowState
include Singleton
def next_state(light)
light.state = RedState.instance
end
def color
"Yellow"
end
end
Practical Examples
Order processing systems demonstrate the State Pattern handling complex workflows. An order transitions through multiple states, each with specific allowed operations and validation rules:
class Order
attr_accessor :state, :items, :total
def initialize
@items = []
@total = 0
@state = NewOrderState.new
end
def add_item(item, price)
state.add_item(self, item, price)
end
def remove_item(item)
state.remove_item(self, item)
end
def submit
state.submit(self)
end
def pay(payment_info)
state.pay(self, payment_info)
end
def ship
state.ship(self)
end
def cancel
state.cancel(self)
end
end
class NewOrderState
def add_item(order, item, price)
order.items << item
order.total += price
puts "Added #{item} for $#{price}"
end
def remove_item(order, item)
order.items.delete(item)
puts "Removed #{item}"
end
def submit(order)
if order.items.empty?
puts "Cannot submit empty order"
else
puts "Submitting order"
order.state = SubmittedOrderState.new
end
end
def pay(order, payment_info)
puts "Submit order before payment"
end
def ship(order)
puts "Cannot ship unsubmitted order"
end
def cancel(order)
puts "Canceling new order"
order.items.clear
end
end
class SubmittedOrderState
def add_item(order, item, price)
puts "Cannot modify submitted order"
end
def remove_item(order, item)
puts "Cannot modify submitted order"
end
def submit(order)
puts "Order already submitted"
end
def pay(order, payment_info)
puts "Processing payment: #{payment_info}"
order.state = PaidOrderState.new
end
def ship(order)
puts "Payment required before shipping"
end
def cancel(order)
puts "Canceling submitted order"
order.state = CanceledOrderState.new
end
end
class PaidOrderState
def add_item(order, item, price)
puts "Cannot modify paid order"
end
def remove_item(order, item)
puts "Cannot modify paid order"
end
def submit(order)
puts "Order already submitted"
end
def pay(order, payment_info)
puts "Order already paid"
end
def ship(order)
puts "Shipping order"
order.state = ShippedOrderState.new
end
def cancel(order)
puts "Canceling paid order - refund required"
order.state = CanceledOrderState.new
end
end
class ShippedOrderState
def add_item(order, item, price)
puts "Cannot modify shipped order"
end
def remove_item(order, item)
puts "Cannot modify shipped order"
end
def submit(order)
puts "Order already processed"
end
def pay(order, payment_info)
puts "Order already paid"
end
def ship(order)
puts "Order already shipped"
end
def cancel(order)
puts "Cannot cancel shipped order - return required"
end
end
class CanceledOrderState
def add_item(order, item, price)
puts "Cannot modify canceled order"
end
def remove_item(order, item)
puts "Cannot modify canceled order"
end
def submit(order)
puts "Order canceled"
end
def pay(order, payment_info)
puts "Order canceled"
end
def ship(order)
puts "Order canceled"
end
def cancel(order)
puts "Order already canceled"
end
end
Media player applications use the State Pattern to manage playback states with different control behaviors:
class MediaPlayer
attr_accessor :state, :media
def initialize(media)
@media = media
@state = StoppedState.new
end
def play
state.play(self)
end
def pause
state.pause(self)
end
def stop
state.stop(self)
end
end
class PlayingState
def play(player)
puts "Already playing"
end
def pause(player)
puts "Pausing #{player.media}"
player.state = PausedState.new
end
def stop(player)
puts "Stopping #{player.media}"
player.state = StoppedState.new
end
end
class PausedState
def play(player)
puts "Resuming #{player.media}"
player.state = PlayingState.new
end
def pause(player)
puts "Already paused"
end
def stop(player)
puts "Stopping #{player.media}"
player.state = StoppedState.new
end
end
class StoppedState
def play(player)
puts "Playing #{player.media}"
player.state = PlayingState.new
end
def pause(player)
puts "Cannot pause stopped media"
end
def stop(player)
puts "Already stopped"
end
end
Design Considerations
The State Pattern suits scenarios where object behavior changes significantly based on internal state and where state transitions follow clearly defined rules. Objects with multiple states that each require different behavior implementations benefit from the pattern. When conditional logic checking the current state appears throughout a class, the State Pattern refactors this code into maintainable state classes.
State machines with complex transition rules represent ideal candidates for the pattern. Systems where states have distinct responsibilities and behaviors gain clarity through state encapsulation. The pattern works well when states need to control their own transitions or when transition logic contains complex validation and business rules.
The pattern introduces overhead compared to simple conditional logic. Creating separate classes for each state requires more code than switch statements or if-else chains. Applications with few states or simple state logic may not justify this additional structure. The decision to use the State Pattern depends on balancing the benefits of maintainability and extensibility against the costs of additional classes and objects.
Alternative approaches include using enums or symbols to represent states with conditional logic handling behavior. This approach reduces complexity for simple state management but concentrates all state-specific code in the context class. As state logic complexity grows, conditional approaches become harder to maintain. The Strategy Pattern resembles the State Pattern but focuses on interchangeable algorithms rather than state-dependent behavior. Strategy objects typically do not change during runtime, while state objects specifically manage state transitions.
The State Pattern compares to the Observer Pattern in systems where state changes trigger notifications. Combining both patterns allows states to notify observers when transitions occur. The Command Pattern encapsulates requests as objects and can represent state transitions as command objects, providing undo functionality for state changes.
Context and state coupling represents a design tradeoff in the pattern. States need context references to trigger transitions, creating bidirectional dependencies. This coupling increases complexity but enables states to manage transitions autonomously. Alternative designs where contexts control all transitions reduce coupling but centralize transition logic in the context, losing the benefits of distributed state management.
Common Patterns
The Singleton State pattern implements state objects as singletons when states contain no context-specific data. This approach reduces memory usage and object creation overhead:
class StateMachine
def initialize
@state = IdleState.instance
end
def process
@state = @state.process
end
end
require 'singleton'
class IdleState
include Singleton
def process
puts "Processing in idle state"
ActiveState.instance
end
end
class ActiveState
include Singleton
def process
puts "Processing in active state"
IdleState.instance
end
end
The State Factory pattern centralizes state creation and management, controlling which state instances get created and reused:
class StateFactory
@states = {}
def self.get_state(state_class)
@states[state_class] ||= state_class.new
end
def self.clear_states
@states.clear
end
end
class Context
attr_reader :state
def initialize
@state = StateFactory.get_state(InitialState)
end
def transition_to(state_class)
@state = StateFactory.get_state(state_class)
end
end
The Hierarchical State pattern organizes states into inheritance hierarchies, allowing state subclasses to inherit common behavior:
class BaseState
def common_operation(context)
puts "Common operation in all states"
end
def state_specific_operation(context)
raise NotImplementedError
end
end
class StateA < BaseState
def state_specific_operation(context)
puts "State A specific operation"
context.state = StateB.new
end
end
class StateB < BaseState
def state_specific_operation(context)
puts "State B specific operation"
context.state = StateA.new
end
end
The Null State pattern provides a default state that handles operations when no active state exists, eliminating nil checks:
class NullState
def handle(context)
puts "No active state - ignoring operation"
end
end
class Context
attr_accessor :state
def initialize
@state = NullState.new
end
def request
state.handle(self)
end
end
Common Pitfalls
State transitions creating circular dependencies cause memory leaks and unexpected behavior. When state objects hold references to contexts and contexts hold references to states, proper cleanup becomes critical:
# Problematic: circular reference
class LeakyState
def initialize(context)
@context = context # Creates circular reference
end
def transition
@context.state = AnotherLeakyState.new(@context)
end
end
# Better: pass context as parameter
class CleanState
def transition(context)
context.state = AnotherCleanState.new
end
end
Forgetting to implement all interface methods in concrete states causes runtime errors. Each state class must implement every method defined in the state interface, even if some methods do nothing in certain states:
# Problematic: incomplete implementation
class IncompleteState
def operation_a(context)
puts "Handling operation A"
end
# Missing operation_b - causes NoMethodError
end
# Better: implement all methods
class CompleteState
def operation_a(context)
puts "Handling operation A"
end
def operation_b(context)
puts "Operation B not available in this state"
end
end
Storing state-specific data in state objects when using shared state instances leads to data corruption. Singleton or reused state objects should remain stateless, with all data stored in the context:
# Problematic: stateful singleton
class StatefulSingleton
include Singleton
def initialize
@data = [] # Shared across all contexts
end
def add_data(item)
@data << item # Corrupts data for all contexts
end
end
# Better: store data in context
class StatelessState
include Singleton
def add_data(context, item)
context.data << item # Each context maintains own data
end
end
class Context
attr_accessor :data
def initialize
@data = []
end
end
State explosion occurs when representing every possible state combination as separate state classes. A system with multiple independent state variables creates a combinatorial explosion of state classes. This pitfall indicates the need for multiple state machines or a different pattern:
# Problematic: state explosion
# Connection can be encrypted/unencrypted and compressed/uncompressed
# Creates 4 state classes: EncryptedCompressed, EncryptedUncompressed,
# UnencryptedCompressed, UnencryptedUncompressed
# Better: use multiple independent state machines
class Connection
attr_accessor :encryption_state, :compression_state
def initialize
@encryption_state = UnencryptedState.new
@compression_state = UncompressedState.new
end
end
Transitioning to states before validating prerequisites causes invalid state transitions. States should validate preconditions before allowing transitions:
# Problematic: unconditional transition
class RiskyState
def transition(context)
context.state = NextState.new # Transitions without validation
end
end
# Better: validate before transition
class SafeState
def transition(context)
if context.ready_for_transition?
context.state = NextState.new
else
puts "Transition preconditions not met"
end
end
end
Reference
State Pattern Components
| Component | Responsibility | Implementation |
|---|---|---|
| Context | Maintains current state reference | Stores state object, delegates requests to state |
| State Interface | Defines common operations | Abstract class or module with operation signatures |
| Concrete States | Implement state-specific behavior | Classes implementing state interface |
| State Transition | Changes context state | State object modifies context state reference |
Implementation Approaches
| Approach | Characteristics | Use Case |
|---|---|---|
| Class-based | Separate class per state | Complex state behavior, multiple operations |
| Module-based | Ruby modules as states | Simple states, behavior mixing |
| Singleton | Shared state instances | Stateless states, memory optimization |
| Method manipulation | Dynamic method definition | Runtime behavior changes, simple states |
State Lifecycle
| Phase | Action | Responsibility |
|---|---|---|
| Creation | Instantiate initial state | Context initializer |
| Operation | Execute state-specific behavior | Current state object |
| Transition | Change to new state | Current state or context |
| Cleanup | Release state resources | Context or state depending on design |
Common State Transition Patterns
| Pattern | Description | Example |
|---|---|---|
| Linear | Sequential state progression | Draft → Review → Published |
| Cyclic | States repeat in cycle | Red → Green → Yellow → Red |
| Branching | Multiple paths from single state | Active → Paused or Active → Stopped |
| Converging | Multiple states lead to common state | Various states → Completed |
Design Decision Matrix
| Factor | Favor State Pattern | Favor Conditional Logic |
|---|---|---|
| Number of states | More than 3 | 2-3 states |
| State complexity | High, distinct behavior per state | Low, similar behavior |
| Transition rules | Complex, state-controlled | Simple, context-controlled |
| Future state additions | Likely | Unlikely |
| Code maintainability priority | High | Medium |
Ruby-Specific Considerations
| Feature | Benefit | Limitation |
|---|---|---|
| Modules as states | No inheritance required | Single module extension at a time |
| Method redefinition | Dynamic behavior changes | Debugging complexity |
| Singleton pattern | Memory efficiency | Thread safety concerns |
| Duck typing | Flexible state interface | Runtime errors for missing methods |