Command: Making Actions First-Class
What This Concept Is
Command takes a request -- "delete this file", "transfer $50", "submit this form" -- and packages it as an object. That object bundles the operation, the target (receiver), and any parameters into one thing you can pass around, store, or defer.
Four roles:
- Receiver -- the object that actually does the work.
- Command -- an object with an
execute()method that calls the receiver. - Invoker -- the thing that triggers commands (a button, a menu, a scheduler).
- Client -- the wiring code that creates the command and associates it with an invoker.
Invoker and receiver do not see each other. They are connected through the command.
Why It Matters Here
Once actions are objects, a lot becomes possible:
- history and undo
- queueing and scheduling
- logging and replay
- remote execution after defining a versioned data representation and handler contract
- macro commands (compose them)
Every one of those requires a way to refer to an operation after the moment it was requested. Commands give you that reference.
Concrete Example
A text editor where buttons, keystrokes, and macros all run through commands.
from abc import ABC, abstractmethod
class Editor:
def __init__(self): self.text = ""
def insert(self, s, at): self.text = self.text[:at] + s + self.text[at:]
def delete(self, start, end): self.text = self.text[:start] + self.text[end:]
class Command(ABC):
@abstractmethod
def execute(self): ...
class InsertCommand(Command):
def __init__(self, editor: Editor, s: str, at: int):
self.editor, self.s, self.at = editor, s, at
def execute(self):
self.editor.insert(self.s, self.at)
class DeleteCommand(Command):
def __init__(self, editor: Editor, start: int, end: int):
self.editor, self.start, self.end = editor, start, end
def execute(self):
self.editor.delete(self.start, self.end)
class Button: # invoker
def __init__(self, cmd: Command): self.cmd = cmd
def click(self): self.cmd.execute()
ed = Editor()
Button(InsertCommand(ed, "hello", 0)).click()
Button(InsertCommand(ed, " world", 5)).click()
print(ed.text) # "hello world"
Button knows nothing about Editor. It only knows "I have a command; on click, execute it." Same Button class, three entirely different effects.
Common Confusion / Misconception
- "It is just a function with a fancy name." In languages with first-class functions, a bound method or a closure is a Command. Named records or objects can make metadata and lifecycle explicit. A class is not automatically serializable, and functions can be wrapped with metadata too.
- Commands that do too much. A command owns one request. It may delegate to a receiver or implement the operation itself; choose a boundary that makes dependencies and invariants clear.
- Mutable commands. If
executewrites back to the command itself beyond capturing undo state, replaying becomes risky. Treat commands as value-ish after construction. - Over-application. Not every method call deserves a command. Wrap actions you will reuse, queue, record, or undo.
How To Use It
- Identify which operations need first-class handling (undo, queue, log, remote).
- Define a
Commandinterface withexecute()(and maybeundo(), see next concept). - For each operation, create a class holding a reference to the receiver and any parameters, with
execute()delegating to the receiver. - Give invokers a field of type
Commandand have them callexecute()when triggered. - Keep construction and execution separate in time; that separation is the whole point.
Check Yourself
- Name the four roles and give one example of each from a UI app.
- Why does the invoker not depend on the receiver?
- What changes about the code the moment an operation becomes first-class?
- When should you not promote a function call into a Command?
Mini Drill or Application
Take a small CLI tool with four operations (add, delete, rename, tag).
- Wrap each in a
Commandclass withexecute(). - Replace the argument-parsing dispatch with a map
{"add": AddCommand, ...}. - Print the command class name before running it -- you now have a diagnostic label, not a complete audit trail.
- Run a test that feeds a list of commands and reports failures per command.
Stop when dispatch, validation, effect ownership and failure are explicit; a small dispatch branch is acceptable.
Read This Only If Stuck
- Head First: 6 The Command Pattern (book index)
- Head First: The Command Pattern Defined (book index)
- Head First: Assigning Commands to Slots (book index)
- GoF: Command Class and Subclasses (book index)
- GoF: Command Implementation (book index)
- Refactoring: Replace Function with Command (book index)
Contract and transfer
Separate an immutable request from one execution's result and undo state. Reusing a mutable command instance for two history entries can overwrite the first entry's backup. A class holding an open socket or editor reference is not a durable message. For deferred work, store operation name, schema version, validated values and target identity, then resolve the receiver at execution time.
The slicing editor sample assumes valid indices and only demonstrates dispatch: Python slicing tolerates out-of-range positions. L03 explicitly rejects invalid edits before changing document or history. Capture authorization and validation requirements at the appropriate boundary; a deserialized command name is not permission to execute an arbitrary method.
Transfer: a queued rename sees that the target was deleted after enqueueing. Define reject, ignore or redirect behavior and record the outcome. Serialization preserves a request's data; it cannot preserve the world that request expected.