CrackedRuby CrackedRuby

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