Before you start
Command is what turns "action" from a verb into a noun. Once actions are objects, you can do things you can't do with plain method calls: queue them, log them, undo them, replay them, serialize them, ship them across a network.
The one-line version
Command wraps each action (execute + undo) in an object so the surrounding system can treat actions like data.
The signal to use Command
Ask: "do I want to treat this action as data?" If yes to any:
- Undo / redo — the classic use case
- Queue for later — job scheduler, dry-run mode
- Log / audit trail — record what happened
- Macro / replay — save a sequence, play it back
- Cross-process action — serialize and send
- Transaction rollback — same shape as undo
Command is the pattern behind ALL of these. Whichever requirement fires first, the shape is the same.
Where Command shows up in real code
- Every editor's undo/redo (Word, Photoshop, VS Code, every IDE ever)
- Database transactions — SQL statements as commands with implicit inverses
- Message queues — each queued message is a command
- Redux / Vuex — dispatched "actions" ARE Commands
- Bash history + Ctrl+R — command replay
- CI systems — pipeline steps as commands
- Automation frameworks — Selenium, Cypress record commands
- Kubernetes rollbacks — deployments as commands
The undo challenge
Commands are easy to execute(). The hard part is
undo(). For InsertCommand, you know how many
characters you added, so undo removes them — simple.
For DeleteCommand, you need to remember what you
deleted so you can put it back. That "remember what I
removed" step must happen at execute() time, because
by undo() time the text that was there is gone.
This lesson tests that specifically. It's a subtle bug
— a naive DeleteCommand that captures state at
__init__ instead of execute() breaks when the
command is applied later than you expected.
The redo chain semantics
Standard editor behavior: after undo(), the undone
action moves to a redo stack. redo() runs it
again. Fine so far.
But if the user does something NEW after undoing, the redo stack must clear. Otherwise you could:
- Type "hello"
- Undo
- Type "world"
- Redo — restore "hello"?? No — that history was invalidated.
Every real editor works this way. Our Editor implements
this via _undone.clear() inside apply(). Missing
this is a common bug.
Command vs Strategy vs Observer
Three behavioral patterns that get mixed up:
- Strategy — one operation, many algorithms. The Strategy object provides the "how".
- Command — one action wrapped as an object so it can be handled like data. The Command object provides the "what" + "how to reverse".
- Observer — an event, notified to N reactions. The Observer callbacks provide the "who cares".
Different problems. Different patterns.
The macro extension
Once you have Command, macros are trivial. A
MacroCommand holds a list of commands, .execute()
runs them all, .undo() runs them all in REVERSE
order.
class MacroCommand(Command):
def __init__(self, commands):
self._cmds = commands
def execute(self, editor):
for c in self._cmds:
c.execute(editor)
def undo(self, editor):
for c in reversed(self._cmds):
c.undo(editor)
That's the composability payoff Command gives you.
The decision cheat-sheet
| Situation | Pattern |
|---|---|
| Actions need undo | Command |
| Actions need to be queued for later | Command |
| Actions need to be logged / replayed | Command |
| Actions need to be sent over the network | Command (+ serialization) |
| One operation, multiple algorithms | Strategy |
| One event, multiple reactions | Observer |
| Object behavior changes based on its state | State (next lesson) |