Skip to main content

Command Pattern

Encapsulate requests as objects to queue, log, and undo operations

TL;DR

Command encapsulates a request as an object, decoupling the sender from the receiver. This enables queuing commands, logging operations, supporting undo/redo, and composing complex sequences. Use it when you need to parameterize objects with actions, queue requests, or provide undo functionality.

Learning Objectives

  • You will understand how commands decouple request senders from executors.
  • You will identify when to encapsulate operations as command objects.
  • You will implement commands with execute and optional undo behavior.
  • You will compose complex operations from simple command objects.

Motivating Scenario

A text editor needs undo/redo functionality. Operations like "insert text," "delete," "format bold" must be reversible. Instead of scattering undo logic throughout the editor, wrap each operation as a Command object. Commands execute actions forward and store state for reversal. A command history tracks all operations, enabling step-by-step undo and redo without entangling undo logic with business logic.

Core Concepts

Command encapsulates a request as an object, capturing the action, receiver, and parameters. This enables storing, queueing, and undoing commands without tight coupling.

Key elements:

  • Command: interface defining execute and optionally undo methods
  • ConcreteCommand: implements the command interface, storing receiver and parameters
  • Receiver: object that performs the actual work
  • Invoker: executes commands and manages their history
  • Client: creates and configures commands
Command structure

Practical Example

Consider a document editor with undo/redo using commands.

command.py
from abc import ABC, abstractmethod

class Document:
def __init__(self):
self.content = ""

def insert(self, text: str, position: int):
self.content = self.content[:position] + text + self.content[position:]

def delete(self, position: int, length: int):
self.content = self.content[:position] + self.content[position+length:]

class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass

@abstractmethod
def undo(self) -> None:
pass

class InsertCommand(Command):
def __init__(self, document: Document, text: str, position: int):
self.document = document
self.text = text
self.position = position

def execute(self) -> None:
self.document.insert(self.text, self.position)

def undo(self) -> None:
self.document.delete(self.position, len(self.text))

class DeleteCommand(Command):
def __init__(self, document: Document, position: int, length: int):
self.document = document
self.position = position
self.length = length
self.deleted_text = None

def execute(self) -> None:
self.deleted_text = self.document.content[self.position:self.position+self.length]
self.document.delete(self.position, self.length)

def undo(self) -> None:
if self.deleted_text:
self.document.insert(self.deleted_text, self.position)

class DocumentEditor:
def __init__(self, document: Document):
self.document = document
self.history = []
self.redo_stack = []

def execute(self, command: Command):
command.execute()
self.history.append(command)
self.redo_stack.clear()

def undo(self):
if self.history:
cmd = self.history.pop()
cmd.undo()
self.redo_stack.append(cmd)

def redo(self):
if self.redo_stack:
cmd = self.redo_stack.pop()
cmd.execute()
self.history.append(cmd)

# Usage
doc = Document()
editor = DocumentEditor(doc)

editor.execute(InsertCommand(doc, "Hello ", 0))
editor.execute(InsertCommand(doc, "World!", 6))
print(f"Content: {doc.content}") # Hello World!

editor.undo()
print(f"After undo: {doc.content}") # Hello

editor.redo()
print(f"After redo: {doc.content}") # Hello World!

When to Use / When Not to Use

Use Command
  1. You need to queue, schedule, or defer operations
  2. Undo/redo functionality is required
  3. You need transaction-like atomic operations
  4. Logging and auditing of operations is important
  5. You want to compose complex operations from simpler ones
Avoid Command
  1. Simple direct method calls are sufficient
  2. No undo/redo or operation history needed
  3. Performance is critical and indirection adds overhead
  4. Commands are trivial with single parameters
  5. Client needs immediate feedback from execution

Patterns and Pitfalls

Design Review Checklist

  • Does each command encapsulate a single, well-defined action?
  • Are undo and execute operations correctly inverse to each other?
  • Is command state captured at execution time, not reference time?
  • Can commands be safely queued and executed asynchronously?
  • Is the invoker's history management thread-safe if needed?
  • Do macro commands correctly compose and decompose into atomic commands?
  • Are unexecuted commands handled to avoid orphaned redo stacks?

Self-Check

  1. How does Command enable undo/redo without scattering reversal logic? Commands encapsulate reversal as the undo() method, keeping forward and reverse logic together.

  2. What's the difference between a command and a simple callback function? Commands are objects that encapsulate both action and state, enabling queuing, logging, and composition beyond simple callbacks.

  3. Can commands be composed into larger commands? Yes—macro commands can contain and execute other commands, enabling complex operations to be undone atomically.

One Takeaway

Command transforms methods into first-class objects, enabling queuing, logging, undo/redo, and composable operations. Use it whenever you need to parameterize, defer, or reverse computations.

Next Steps

References

  • Gang of Four, "Design Patterns: Elements of Reusable Object-Oriented Software"
  • Refactoring Guru's Command ↗️
  • Martin Fowler on Undo/Redo Architecture ↗️