Overview
The Command Pattern transforms requests into standalone objects containing all information about the request. This transformation decouples the object that invokes the operation from the object that performs it, creating a layer of abstraction between senders and receivers.
The pattern originated from the need to issue requests without knowing the specific operation being requested or the receiver performing it. Traditional method calls create tight coupling between caller and callee, making it difficult to parameterize objects with operations, queue operations, or implement undo functionality. The Command Pattern addresses these limitations by reifying requests as first-class objects.
The pattern consists of four primary participants. The Command declares an interface for executing operations. Concrete Command classes implement the Command interface and define bindings between Receiver objects and actions. The Receiver contains the business logic that performs actual work. The Invoker stores commands and triggers their execution. The Client creates Command objects and sets their receivers.
This separation enables several capabilities not possible with direct method calls. Commands can be stored in data structures, passed as method arguments, and executed at different times. The same command can be reused with different receivers. Operations can be logged, queued, and undone. Invokers remain ignorant of concrete command classes and receiver implementation details.
# Basic command structure
class Command
def execute
raise NotImplementedError
end
end
class ConcreteCommand < Command
def initialize(receiver)
@receiver = receiver
end
def execute
@receiver.action
end
end
class Receiver
def action
puts "Receiver performing action"
end
end
# Usage
receiver = Receiver.new
command = ConcreteCommand.new(receiver)
command.execute
# => Receiver performing action
Key Principles
The Command Pattern operates on the principle of encapsulation. Each command encapsulates a single request as an object, packaging together the method call, the receiver object, and any parameters required for execution. This encapsulation transforms imperative requests into declarative objects.
The pattern establishes clear separation between three concerns: what operations are performed, when operations are performed, and how operations are performed. Commands define what operations to execute. Invokers determine when to execute commands. Receivers implement how operations are carried out. This separation creates flexibility points at each level.
Commands expose a uniform interface regardless of their internal complexity. The execute method provides a consistent entry point for all operations. Invokers interact exclusively with this interface, remaining decoupled from concrete command implementations. This uniformity enables treating diverse operations through a common protocol.
The pattern supports two models of command implementation. The first model stores receiver references and delegates work during execution. The second model implements operations directly within the command, eliminating separate receivers. The choice depends on whether commands represent wrappers around existing functionality or self-contained operations.
Command objects maintain state between creation and execution. Constructor parameters establish receiver bindings and capture request parameters. This state persists throughout the command lifecycle, enabling deferred execution. Commands can be created in one context and executed in another, with state bridging temporal and spatial gaps.
The pattern enables composition of commands. Macro commands aggregate multiple commands and execute them as a unit. Command sequences implement workflows. Conditional commands select between alternatives based on runtime conditions. These compositions treat individual commands and composites uniformly through the common interface.
Undo functionality requires commands to maintain execution state. Commands must either store prior state before execution or capture sufficient information to reverse operations. The pattern extends the command interface with an unexecute method that reverses effects. Invokers maintain command history stacks to support multi-level undo.
# Command with undo support
class UndoableCommand
def execute
raise NotImplementedError
end
def unexecute
raise NotImplementedError
end
end
class AddTextCommand < UndoableCommand
def initialize(document, text)
@document = document
@text = text
@position = nil
end
def execute
@position = @document.cursor_position
@document.insert(@text)
end
def unexecute
@document.delete(@position, @text.length)
end
end
The pattern transforms temporal coupling into explicit object relationships. Traditional method calls execute immediately within the caller's context. Commands separate invocation time from execution time. This temporal decoupling enables queueing, scheduling, and transaction-like operations where multiple requests are grouped and executed atomically.
Ruby Implementation
Ruby provides several features that simplify Command Pattern implementation. Blocks and Procs offer lightweight alternatives to command classes for simple cases. Lambdas provide command-like behavior with closure semantics. Method objects encapsulate method calls as first-class objects. The choice between these approaches depends on required functionality and complexity.
The simplest command implementation uses classes with execute methods. Ruby's dynamic typing eliminates the need for explicit command interfaces. Duck typing allows any object with an execute method to function as a command. This flexibility reduces boilerplate compared to statically-typed languages.
class LightOnCommand
def initialize(light)
@light = light
end
def execute
@light.turn_on
end
def unexecute
@light.turn_off
end
end
class Light
def turn_on
puts "Light is on"
end
def turn_off
puts "Light is off"
end
end
light = Light.new
command = LightOnCommand.new(light)
command.execute
# => Light is on
command.unexecute
# => Light is off
For commands without undo requirements or complex state, Procs provide a concise alternative. The Proc captures the receiver and operation as a closure. This approach works well for simple operations that don't require explicit undo logic.
class RemoteControl
def initialize
@commands = {}
end
def set_command(slot, on_command, off_command)
@commands[slot] = { on: on_command, off: off_command }
end
def press_button(slot, state)
@commands[slot][state].call
end
end
fan = Fan.new
remote = RemoteControl.new
remote.set_command(
:fan,
-> { fan.high },
-> { fan.off }
)
remote.press_button(:fan, :on)
Ruby modules enable sharing command behavior across classes. A command module can define common functionality like logging or timing. Concrete command classes include the module to inherit shared behavior. This approach promotes code reuse without inheritance hierarchies.
module Loggable
def execute
log_before
result = super
log_after
result
end
private
def log_before
puts "Executing #{self.class.name}"
end
def log_after
puts "Completed #{self.class.name}"
end
end
class SaveCommand
include Loggable
def initialize(document)
@document = document
end
def execute
@document.save
end
end
Method objects provide another implementation approach. Ruby's method method returns a Method object representing a bound method call. These objects support the call interface, making them compatible with command semantics. Method objects capture the receiver implicitly, reducing command class proliferation.
class Calculator
def add(x, y)
x + y
end
def subtract(x, y)
x - y
end
end
calc = Calculator.new
add_command = calc.method(:add)
result = add_command.call(5, 3)
# => 8
Command queues leverage Ruby's Array and Queue classes. Commands accumulate in collections and execute in order. Thread-safe queues support concurrent command execution. Priority queues implement command scheduling based on precedence.
class CommandQueue
def initialize
@commands = []
end
def add(command)
@commands << command
end
def execute_all
@commands.each(&:execute)
@commands.clear
end
end
queue = CommandQueue.new
queue.add(LightOnCommand.new(bedroom_light))
queue.add(LightOnCommand.new(kitchen_light))
queue.execute_all
Thread-based command execution requires thread-safe implementations. Commands executing across threads must handle shared state carefully. Ruby's Mutex class provides synchronization. Thread-local storage isolates per-thread state.
require 'thread'
class AsyncCommandExecutor
def initialize
@queue = Queue.new
@thread = Thread.new { run_loop }
end
def submit(command)
@queue << command
end
private
def run_loop
loop do
command = @queue.pop
command.execute
end
end
end
Design Considerations
The Command Pattern suits scenarios requiring operation parameterization. When objects need to be configured with operations at runtime, commands provide the necessary indirection. Callbacks, event handlers, and menu actions benefit from command-based designs. The pattern excels when operations must be specified independently of their receivers.
Command Pattern adds complexity through additional classes and indirection. Simple applications with direct method calls may not justify this overhead. The pattern's benefits emerge when dealing with undo/redo, operation queuing, transaction management, or macro recording. Projects without these requirements should consider simpler alternatives.
The pattern competes with several alternatives. Strategy Pattern also encapsulates algorithms as objects but focuses on algorithmic variation rather than request encapsulation. Callback functions provide lightweight operation parameterization without command infrastructure. Event systems offer publish-subscribe semantics for decoupled communication. The choice depends on whether the problem involves encapsulating requests or selecting algorithms.
Commands introduce memory overhead through object allocation. Each command instance occupies memory until execution completes. Long-lived command queues or extensive undo histories accumulate objects. Memory-constrained environments may require command pooling or history limits. The overhead typically remains negligible compared to the flexibility gained.
Undo support significantly increases implementation complexity. Commands must capture or store state needed for reversal. Some operations resist undo implementation, particularly those with external side effects like network requests or file deletions. Compensating transactions may be necessary for irreversible operations. The undo interface should clearly document which commands support reversal.
Thread safety requires careful consideration in concurrent environments. Commands executing on multiple threads must handle shared state appropriately. Immutable commands avoid synchronization overhead. Mutable commands need explicit locking. Command queues require thread-safe implementations. The threading model should be decided early as retrofitting thread safety proves difficult.
Command lifetimes vary based on usage patterns. Transient commands execute once and become eligible for garbage collection. Persistent commands remain in undo stacks or queues for extended periods. Serializable commands may be saved to disk for durability. Lifetime requirements influence state management and resource cleanup strategies.
The pattern's granularity affects design flexibility. Fine-grained commands represent atomic operations, enabling flexible composition but increasing class count. Coarse-grained commands bundle multiple operations, reducing class proliferation but limiting reuse. The appropriate granularity depends on composition requirements and operation atomicity.
# Fine-grained commands
class SetWidthCommand
def initialize(shape, width)
@shape = shape
@width = width
@old_width = nil
end
def execute
@old_width = @shape.width
@shape.width = @width
end
def unexecute
@shape.width = @old_width
end
end
# Coarse-grained command
class ResizeShapeCommand
def initialize(shape, width, height)
@shape = shape
@width = width
@height = height
@old_dimensions = nil
end
def execute
@old_dimensions = { width: @shape.width, height: @shape.height }
@shape.width = @width
@shape.height = @height
end
def unexecute
@shape.width = @old_dimensions[:width]
@shape.height = @old_dimensions[:height]
end
end
Common Patterns
Macro commands aggregate multiple commands into composite operations. The macro command stores a collection of subcommands and executes them sequentially. This pattern implements transaction-like behavior where multiple operations execute as a unit. Macro undo reverses subcommands in reverse order to maintain consistency.
class MacroCommand
def initialize
@commands = []
end
def add(command)
@commands << command
end
def execute
@commands.each(&:execute)
end
def unexecute
@commands.reverse_each(&:unexecute)
end
end
# Creating a macro
open_file = OpenFileCommand.new(editor, "document.txt")
find_text = FindCommand.new(editor, "TODO")
replace_text = ReplaceCommand.new(editor, "DONE")
macro = MacroCommand.new
macro.add(open_file)
macro.add(find_text)
macro.add(replace_text)
macro.execute
Command history implements undo/redo functionality through stacks. The history maintains executed commands in a stack. Undo pops commands from the history, calls unexecute, and pushes them to a redo stack. Redo reverses this process. History limits prevent unbounded memory growth.
class CommandHistory
def initialize(limit = 100)
@history = []
@undone = []
@limit = limit
end
def execute(command)
command.execute
@history.push(command)
@history.shift if @history.length > @limit
@undone.clear
end
def undo
return if @history.empty?
command = @history.pop
command.unexecute
@undone.push(command)
end
def redo
return if @undone.empty?
command = @undone.pop
command.execute
@history.push(command)
end
def can_undo?
!@history.empty?
end
def can_redo?
!@undone.empty?
end
end
Null commands implement the Null Object pattern within the command context. Null commands provide do-nothing implementations of the command interface. They eliminate nil checks in client code and provide default behaviors. This pattern proves useful for optional commands or unassigned slots.
class NullCommand
def execute
# Do nothing
end
def unexecute
# Do nothing
end
end
class RemoteControl
def initialize
@commands = Hash.new(NullCommand.new)
end
def set_command(slot, command)
@commands[slot] = command
end
def press_button(slot)
@commands[slot].execute
end
end
Command processors add cross-cutting concerns to command execution. Processors wrap commands and inject behavior like logging, timing, or validation. The decorator pattern enables stacking multiple processors. This approach centralizes concerns rather than duplicating code across command classes.
class LoggingProcessor
def initialize(command)
@command = command
end
def execute
puts "Starting: #{@command.class.name}"
start_time = Time.now
result = @command.execute
elapsed = Time.now - start_time
puts "Completed in #{elapsed}s"
result
end
end
class ValidatingProcessor
def initialize(command)
@command = command
end
def execute
raise "Invalid command state" unless @command.valid?
@command.execute
end
end
Transactional commands implement all-or-nothing semantics. The command executes multiple operations within a transaction boundary. If any operation fails, the entire transaction rolls back. This pattern requires commands to support partial undo and rollback checkpoints.
class TransactionalCommand
def initialize(commands)
@commands = commands
@executed = []
end
def execute
@commands.each do |cmd|
begin
cmd.execute
@executed << cmd
rescue StandardError => e
rollback
raise e
end
end
end
private
def rollback
@executed.reverse_each do |cmd|
cmd.unexecute rescue nil
end
@executed.clear
end
end
Scheduled commands defer execution until specified conditions are met. Time-based scheduling executes commands at future times. Condition-based scheduling waits for specific states. This pattern requires a scheduler that monitors conditions and triggers execution.
class ScheduledCommand
def initialize(command, execute_at)
@command = command
@execute_at = execute_at
end
def ready?
Time.now >= @execute_at
end
def execute
@command.execute if ready?
end
end
class CommandScheduler
def initialize
@scheduled = []
end
def schedule(command, delay)
execute_at = Time.now + delay
@scheduled << ScheduledCommand.new(command, execute_at)
end
def process
@scheduled.select(&:ready?).each do |cmd|
cmd.execute
@scheduled.delete(cmd)
end
end
end
Practical Examples
Text editor undo/redo demonstrates classic Command Pattern application. Each editing operation becomes a command supporting both execution and reversal. The editor maintains command history enabling multi-level undo. Commands capture document state needed for accurate undo.
class Document
attr_accessor :content, :cursor
def initialize
@content = ""
@cursor = 0
end
def insert(text)
@content.insert(@cursor, text)
@cursor += text.length
end
def delete(position, length)
@content.slice!(position, length)
@cursor = position
end
end
class InsertCommand
def initialize(document, text)
@document = document
@text = text
@position = nil
end
def execute
@position = @document.cursor
@document.insert(@text)
end
def unexecute
@document.delete(@position, @text.length)
end
end
class DeleteCommand
def initialize(document, length)
@document = document
@length = length
@position = nil
@deleted_text = nil
end
def execute
@position = @document.cursor
@deleted_text = @document.content[@position, @length]
@document.delete(@position, @length)
end
def unexecute
@document.cursor = @position
@document.insert(@deleted_text)
end
end
# Usage
doc = Document.new
history = CommandHistory.new
# Type "Hello"
insert = InsertCommand.new(doc, "Hello")
history.execute(insert)
# Type " World"
insert2 = InsertCommand.new(doc, " World")
history.execute(insert2)
puts doc.content
# => Hello World
history.undo
puts doc.content
# => Hello
history.redo
puts doc.content
# => Hello World
Database transaction management leverages commands for atomicity. Each database operation becomes a command. A transaction aggregates commands and executes them within a single database transaction. Failure triggers rollback of all operations.
class DatabaseCommand
def initialize(connection)
@connection = connection
end
def execute
raise NotImplementedError
end
def compensate
raise NotImplementedError
end
end
class InsertRecordCommand < DatabaseCommand
def initialize(connection, table, record)
super(connection)
@table = table
@record = record
@inserted_id = nil
end
def execute
@inserted_id = @connection.insert(@table, @record)
end
def compensate
@connection.delete(@table, @inserted_id) if @inserted_id
end
end
class UpdateRecordCommand < DatabaseCommand
def initialize(connection, table, id, updates)
super(connection)
@table = table
@id = id
@updates = updates
@old_values = nil
end
def execute
@old_values = @connection.get(@table, @id)
@connection.update(@table, @id, @updates)
end
def compensate
@connection.update(@table, @id, @old_values) if @old_values
end
end
class DatabaseTransaction
def initialize(connection)
@connection = connection
@commands = []
@executed = []
end
def add(command)
@commands << command
end
def commit
@connection.begin_transaction
@commands.each do |cmd|
cmd.execute
@executed << cmd
end
@connection.commit
rescue StandardError => e
rollback
raise e
end
private
def rollback
@connection.rollback
@executed.reverse_each { |cmd| cmd.compensate }
end
end
Home automation systems use commands to control devices. Each device action becomes a command. Remote controls trigger commands without knowing device details. Macro commands implement scenes combining multiple device operations.
class SmartHome
class Light
attr_reader :level
def initialize(name)
@name = name
@level = 0
end
def set_level(level)
@level = level.clamp(0, 100)
puts "#{@name} set to #{@level}%"
end
end
class Thermostat
attr_reader :temperature
def initialize
@temperature = 72
end
def set_temperature(temp)
@temperature = temp
puts "Temperature set to #{temp}°F"
end
end
end
class LightCommand
def initialize(light, level)
@light = light
@level = level
@previous_level = nil
end
def execute
@previous_level = @light.level
@light.set_level(@level)
end
def unexecute
@light.set_level(@previous_level)
end
end
class ThermostatCommand
def initialize(thermostat, temperature)
@thermostat = thermostat
@temperature = temperature
@previous_temp = nil
end
def execute
@previous_temp = @thermostat.temperature
@thermostat.set_temperature(@temperature)
end
def unexecute
@thermostat.set_temperature(@previous_temp)
end
end
# Create scene
living_room_light = SmartHome::Light.new("Living Room")
bedroom_light = SmartHome::Light.new("Bedroom")
thermostat = SmartHome::Thermostat.new
evening_scene = MacroCommand.new
evening_scene.add(LightCommand.new(living_room_light, 75))
evening_scene.add(LightCommand.new(bedroom_light, 30))
evening_scene.add(ThermostatCommand.new(thermostat, 68))
evening_scene.execute
# => Living Room set to 75%
# => Bedroom set to 30%
# => Temperature set to 68°F
Job scheduling systems queue commands for asynchronous execution. Commands represent background tasks. The scheduler processes queued commands based on priority and resource availability. Failed commands can be retried or moved to error queues.
class BackgroundJob
attr_reader :priority, :created_at
def initialize(priority: 5)
@priority = priority
@created_at = Time.now
end
def execute
raise NotImplementedError
end
end
class EmailJob < BackgroundJob
def initialize(recipient, subject, body)
super()
@recipient = recipient
@subject = subject
@body = body
end
def execute
# Send email
puts "Sending email to #{@recipient}: #{@subject}"
end
end
class DataProcessingJob < BackgroundJob
def initialize(dataset, priority: 5)
super(priority: priority)
@dataset = dataset
end
def execute
puts "Processing dataset: #{@dataset}"
# Perform data processing
end
end
class JobQueue
def initialize
@queue = []
@failed = []
end
def enqueue(job)
@queue << job
@queue.sort_by! { |j| [-j.priority, j.created_at] }
end
def process_next
return if @queue.empty?
job = @queue.shift
job.execute
rescue StandardError => e
@failed << { job: job, error: e }
puts "Job failed: #{e.message}"
end
def process_all
process_next until @queue.empty?
end
def failed_jobs
@failed
end
end
# Usage
queue = JobQueue.new
queue.enqueue(EmailJob.new("user@example.com", "Welcome", "Hello!"))
queue.enqueue(DataProcessingJob.new("sales_data.csv", priority: 10))
queue.process_all
Reference
Command Pattern Components
| Component | Responsibility | Implementation |
|---|---|---|
| Command | Declares execution interface | Abstract class or module defining execute method |
| ConcreteCommand | Implements operation and binds receiver | Class implementing execute with receiver delegation |
| Receiver | Performs actual work | Domain object with business logic methods |
| Invoker | Triggers command execution | Stores and invokes commands without knowing details |
| Client | Creates and configures commands | Instantiates commands and sets receivers |
Command Interface Methods
| Method | Purpose | Required |
|---|---|---|
| execute | Performs the command operation | Yes |
| unexecute | Reverses the command operation | No |
| valid? | Validates command state | No |
| compensate | Provides rollback for irreversible operations | No |
Command Pattern Variations
| Pattern | Description | Use Case |
|---|---|---|
| Simple Command | Basic execute method only | One-way operations without undo |
| Undoable Command | Adds unexecute method | Text editors, drawing applications |
| Macro Command | Aggregates multiple commands | Composite operations, scenes |
| Transactional Command | All-or-nothing execution | Database operations, distributed systems |
| Queued Command | Deferred execution | Background jobs, task scheduling |
| Logged Command | Records execution history | Audit trails, debugging |
| Remote Command | Serializable for network transmission | Distributed systems, RPC |
Implementation Approaches
| Approach | Advantages | Disadvantages |
|---|---|---|
| Class-based | Full OOP features, clear structure | Increased class count |
| Proc-based | Concise for simple commands | Limited undo support |
| Method objects | Leverages Ruby method system | Less explicit command structure |
| Module mixins | Shared behavior reuse | Potential mixin conflicts |
Common Command Operations
| Operation | Implementation | Complexity |
|---|---|---|
| Execute | Call receiver method with parameters | O(1) |
| Undo | Restore previous state or apply inverse | O(1) to O(n) |
| Redo | Re-execute previously undone command | O(1) |
| Serialize | Convert command to portable format | O(n) where n is state size |
| Clone | Create independent command copy | O(n) where n is state size |
Design Decision Matrix
| Requirement | Recommended Approach | Justification |
|---|---|---|
| Simple callbacks | Proc or lambda | Minimal overhead, sufficient functionality |
| Undo support | Class-based command | Need state storage and unexecute logic |
| Command queuing | Class-based with history | Explicit control over execution timing |
| Transaction support | Transactional command pattern | Requires rollback and compensation |
| Cross-cutting concerns | Command processor wrappers | Centralizes logging, validation, etc. |
| Macro operations | Macro command composite | Groups related operations |
| Async execution | Thread-safe queue | Handles concurrent execution |
State Management Strategies
| Strategy | Description | Memory Impact |
|---|---|---|
| Memento | Store complete prior state | High for large objects |
| Delta | Store only changed values | Low to medium |
| Reverse operation | Calculate inverse operation | Minimal |
| Copy-on-write | Share immutable state | Low with structural sharing |
Undo Implementation Patterns
| Pattern | Mechanism | Limitations |
|---|---|---|
| State restoration | Save and restore receiver state | Memory intensive |
| Inverse operation | Apply opposite operation | Not all operations have inverses |
| Compensation | Execute compensating transaction | Approximate reversal only |
| Snapshot chain | Link commands with state snapshots | Complex lifecycle management |