Design Patterns (GoF and modern) Module 4 – Behavioral patterns: algorithms and requests
Command: turn requests into objects
Wrap each action as an object with execute and undo, then build undo and redo stacks, macros and a command log that is replayed after a restart.
What you will learn
- Wrap actions as command objects with execute and undo
- Build undo and redo stacks, macro commands and a command queue
- Represent commands as closures or enums where that is simpler
- Design commands that are safe to retry and replay by giving them identities
Before you start
What you’ll learn · Before you start
What you will learn
- Wrap actions as command objects with execute and undo
- Build undo and redo stacks, macro commands and a command queue
- Represent commands as closures or enums where that is simpler
- Design commands that are safe to retry and replay by giving them identities
Before you start
On this page
Command is a design pattern that turns a request into an object. The object holds everything needed to carry the request out later: what to do, what to do it to and with which values. The code that runs it needs to know nothing about any of that, only how to say “execute”. Once a request is an object it can be kept, and keeping it is the point: in a history, so it can be undone; in a queue, so a worker can run it later; in a log, so it can be replayed after a crash; in a list, so several requests run as one macro. The Gang of Four catalogue gives exactly those uses: parameterising clients with requests, queueing and logging them, and supporting undo.
Reach for Command when an action must outlive the moment it was asked for: undo and redo, background jobs, retries, an audit trail of who did what.
The problem: actions that leave no trace
An editor’s toolbar that calls buffer.insert(pos, text) directly works until the first request for undo. The call
changed the text and then vanished: nothing remembers what was inserted, where, or what a delete removed. Adding undo
to that design means teaching the buffer about every edit any button can make, and adding a new button means changing
the buffer again. Queueing work for later or retrying it after a failure hits the same wall, because there is no
object to put in the queue.
The pattern: invoker, command and receiver
Command splits the action into four roles. The command interface has execute() and, for undo, undo(). Each
concrete command, such as an insert or a delete, stores its arguments and a reference to the receiver, the
object that does the real work (here the text buffer). The invoker runs commands and keeps them; here it is a
history with two stacks. The client builds commands and hands them to the invoker:
Command in the editor example: the invoker, the command interface, three commands and the receiver
Text description of the diagram
Six class boxes. Each box shows the class name, then its attributes, then its operations; a plus sign marks a public member and a minus sign a private one, and the type follows each name.
At the top is History, labelled the invoker. It has two private lists of commands, done and undone, and three public operations: run(command), undo() and redo(). A line with a hollow diamond at History and an arrowhead at Command, labelled "keeps 0..* on two stacks", says that a history holds any number of commands.
Below it is Command, marked «interface», with two operations: execute() and undo().
Three classes in the bottom row realise Command, each joined to it by a dashed line with a hollow triangle at Command. Macro has a private list of commands; its line is labelled "runs and undoes its steps". Delete has a private pos, n and removed, the text it took out, which it needs for undo. Insert has a private pos and s, the text it adds.
To the right of Command is TextBuffer, labelled the receiver, with a public attribute text and operations insert(pos, s) and delete(pos, n), which returns the deleted text. Solid arrows labelled "edits" run from Insert and from Delete to TextBuffer: those commands do their work by calling the buffer.
The buffer knows nothing about undo, and the history knows nothing about text. A new kind of edit is a new command class; neither of them changes.
Undo and redo with two stacks
The history runs each command and pushes it onto an undo stack. Undo pops the last command, calls its undo() and
pushes it onto a redo stack; redo moves it back the other way. A macro is a command made of commands, which executes
its steps in order and undoes them in reverse:
# A text buffer edited through commands. Each command keeps what it needs to run and to undo itself; the history
# (the invoker) runs commands and keeps them on two stacks, one to undo and one to redo.
class TextBuffer: # the receiver: it does the work and knows nothing about commands
def __init__(self, text=""):
self.text = text
def insert(self, pos, s):
self.text = self.text[:pos] + s + self.text[pos:]
def delete(self, pos, n):
removed = self.text[pos:pos + n]
self.text = self.text[:pos] + self.text[pos + n:]
return removed
class Insert:
def __init__(self, buffer, pos, s):
self.buffer, self.pos, self.s = buffer, pos, s
def execute(self):
self.buffer.insert(self.pos, self.s)
def undo(self):
self.buffer.delete(self.pos, len(self.s))
def __str__(self):
return f"insert {self.s!r} at {self.pos}"
class Delete:
def __init__(self, buffer, pos, n):
self.buffer, self.pos, self.n = buffer, pos, n
self.removed = ""
def execute(self):
self.removed = self.buffer.delete(self.pos, self.n) # kept for undo
def undo(self):
self.buffer.insert(self.pos, self.removed)
def __str__(self):
return f"delete {self.n} at {self.pos}"
class Macro:
"""Several commands that run, and undo, as one."""
def __init__(self, name, commands):
self.name, self.commands = name, commands
def execute(self):
for c in self.commands:
c.execute()
def undo(self):
for c in reversed(self.commands):
c.undo()
def __str__(self):
return f"macro {self.name!r} ({len(self.commands)} steps)"
class History: # the invoker: runs commands and remembers them
def __init__(self):
self.done, self.undone = [], []
def run(self, command):
command.execute()
self.done.append(command)
self.undone.clear() # a new command makes the old redo steps meaningless
def undo(self):
command = self.done.pop()
command.undo()
self.undone.append(command)
return command
def redo(self):
command = self.undone.pop()
command.execute()
self.done.append(command)
return command
buf = TextBuffer("Meet at 10")
h = History()
def show(what):
print(f"{what:<34} {buf.text!r:<34} undo {len(h.done)} redo {len(h.undone)}")
show("start")
for command in [Insert(buf, 10, " tomorrow"), Delete(buf, 8, 2), Insert(buf, 8, "11")]:
h.run(command)
show(str(command))
h.run(Macro("sign", [Insert(buf, len(buf.text), "."), Insert(buf, len(buf.text) + 1, " -- Asha")]))
show(str(h.done[-1]))
show(f"undo {h.undo()}")
show(f"undo {h.undo()}")
show(f"redo {h.redo()}")
h.run(Insert(buf, 0, "Team: "))
show("insert 'Team: ' at 0") Output
start 'Meet at 10' undo 0 redo 0 insert ' tomorrow' at 10 'Meet at 10 tomorrow' undo 1 redo 0 delete 2 at 8 'Meet at tomorrow' undo 2 redo 0 insert '11' at 8 'Meet at 11 tomorrow' undo 3 redo 0 macro 'sign' (2 steps) 'Meet at 11 tomorrow. -- Asha' undo 4 redo 0 undo macro 'sign' (2 steps) 'Meet at 11 tomorrow' undo 3 redo 1 undo insert '11' at 8 'Meet at tomorrow' undo 2 redo 2 redo insert '11' at 8 'Meet at 11 tomorrow' undo 3 redo 1 insert 'Team: ' at 0 'Team: Meet at 11 tomorrow' undo 4 redo 0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 editor.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Each command keeps exactly what its undo needs. An insert can be undone from its position and the length of its text.
A delete cannot: it has to remember the text it removed, which it only knows once it has run, so it saves it in
execute(). The macro sign came back with a single undo, and the last line shows the rule that keeps redo honest:
running a new command empties the redo stack, because the undone edits were made for a text that no longer exists.
Choices every command design makes
The example settles the roles; real systems make five more decisions, and each has a name:
- Inverse commands undo by applying the opposite edit, keeping only the data that edit needs.
- Snapshots, the Memento pattern, undo by restoring a saved copy of the whole state taken before the command.
- Atomic macros undo the steps that already ran when a later step fails, so a macro happens fully or not at all.
- Closures and enums replace command classes: a closure is the lightest command, and an enum is a command that can be printed and parsed.
- Command logs record every command before it runs, so replaying the log rebuilds the state after a restart.
- Command IDs let a receiver recognise a retried command and apply it once.
Practise: undo and redo for a spreadsheet
Exercise · Medium · Python
Undo and redo for a spreadsheet cell editor
sheet.py edits spreadsheet cells directly, so nothing can be taken back. Give it undo and redo by running every edit as a command.
set(cell, value),clear(cell)andbulk_set({cell: value, …})each become one command that remembers what it needs to undo itself: the value each cell had before, or that it had none.undo()undoes the last edit that has not been undone and returnsTrue; with nothing to undo it returnsFalseand changes nothing.redo()does the same in the other direction.bulk_setis one step: oneundo()takes back all of its cells, and oneredo()puts them back.- A new edit after an undo empties the redo stack: the undone edits can no longer be redone.
The sample tests check five sequences: a set that is undone and redone, two sets on one cell, a clear, a bulk set, and a new edit after an undo.
Starter code · sheet.py
class Sheet:
"""Spreadsheet cells by name ("A1", "B2"), edited through set, clear and bulk_set, with undo and redo.
undo() takes back the last edit that has not been undone and returns True, or returns False when there is
nothing to undo. redo() puts back the last edit that was undone and returns True, or returns False when there is
nothing to redo. bulk_set() is one edit: one undo takes all of its cells back. Any new edit after an undo means
the undone edits can no longer be redone.
"""
def __init__(self):
self.cells = {}
def get(self, cell):
return self.cells.get(cell)
def set(self, cell, value):
self.cells[cell] = value
def clear(self, cell):
self.cells.pop(cell, None)
def bulk_set(self, values):
for cell, value in values.items():
self.cells[cell] = value
def undo(self):
# Replace this with your code: run every edit as a command that knows how to undo itself.
return False
def redo(self):
# Replace this with your code.
return False The sample tests · test_sheet.py
from sheet import Sheet
def test_undo_and_redo_a_set():
"""undoes a set, then redoes it"""
s = Sheet()
s.set("A1", 10)
assert s.undo() is True
assert s.get("A1") is None
assert s.redo() is True
assert s.get("A1") == 10
def test_undo_brings_back_the_previous_value():
"""restores the value a cell had before it was overwritten"""
s = Sheet()
s.set("A1", 1)
s.set("A1", 2)
s.undo()
assert s.get("A1") == 1
s.undo()
assert s.get("A1") is None
assert s.undo() is False
def test_undo_a_clear():
"""puts a cleared value back"""
s = Sheet()
s.set("B2", "total")
s.clear("B2")
assert s.get("B2") is None
s.undo()
assert s.get("B2") == "total"
def test_bulk_set_is_one_step():
"""takes back every cell of a bulk_set with one undo, and puts them back with one redo"""
s = Sheet()
s.set("A1", 5)
s.bulk_set({"A1": 1, "B1": 2, "C1": 3})
s.undo()
assert [s.get(c) for c in ("A1", "B1", "C1")] == [5, None, None]
s.redo()
assert [s.get(c) for c in ("A1", "B1", "C1")] == [1, 2, 3]
def test_a_new_edit_clears_redo():
"""refuses to redo after a new edit"""
s = Sheet()
s.set("A1", 1)
s.undo()
s.set("B1", 2)
assert s.redo() is False
assert s.get("A1") is None
assert s.get("B1") == 2 A hint
One command class is enough: an edit that gives some cells new values, where None clears a cell. In execute(), first save each affected cell's current value (or a marker meaning "this cell was empty"), then apply the change; undo() puts the saved values back. set, clear and bulk_set then only build that command and hand it to one method that executes it, pushes it on the undo stack and empties the redo stack.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
Check yourself
3 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
These questions check the roles and the two-stack rule.
Undo that holds up: inverse commands, snapshots and macros, tested Pro: opens with a Pro, Premium or Ultimate pass
Undo is where command designs fail in practice. Here the two ways to undo are measured against each other, macros are made all-or-nothing, and a seeded replay of 200 edits catches a bug that the usual undo test misses.
In this part:
- Inverse commands or snapshots: what each keeps, measured
- The redo stack after a new command: a stale redo reproduced
- A seeded replay that tests the redo rule
- Macros that change everything or nothing
4 runnable examples (Python, commands.py) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Commands as data: closures, enums, logs and retries Pro: opens with a Pro, Premium or Ultimate pass
A command does not have to be a class, and a command that is plain data can be written down, sent and replayed. This part compares the lightweight forms in the track’s languages and shows where each one stops working.
In this part:
- Closures and partial: the lightest commands, and their loop trap
- The same trap in Go, JavaScript and Java
- Enums and tagged records that can be logged and replayed
- Command IDs: a retry that changes nothing
4 runnable examples (Go, Java, Java: counting loop, JavaScript, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
How is the Command pattern different from the Strategy pattern?
A strategy is a way of doing a job: the context calls it to work something out, such as a shipping cost, and keeps using it. A command is a job to be done: an object that carries one request with its arguments, such as "insert this text here", so that it can run later, wait in a queue, be logged, retried or undone. Strategies are usually chosen once from configuration and hold no data about a single call; a new command is made for every request and holds that request's data. Both can be written as functions in most languages, so the difference is the intent: how, against what.
-
Mid to senior
An image editor offers 500 steps of undo on 50 MB images. A snapshot of the whole image after every step would need about 25 GB. How would you design undo so that it fits in memory, and what does each choice cost?
-
Mid to senior
A worker runs ChargeCard commands from a queue. It crashed after the card was charged but before it acknowledged the message, so the queue delivered the command again. How do you make sure the customer is charged once?
-
Mid
A macro command applies five edits to a document, and the fourth one fails. What state should the document end in, and how do you implement that? What changes when the edits go to a server one by one?
Key takeaways
- Command turns a request into an object that holds what to do, to what and with which values, so it can be kept and run later.
- The invoker runs and keeps commands; the receiver does the work; neither knows about the other.
- Two stacks give undo and redo, and every new command empties the redo stack.
- A command keeps what its undo needs; a delete saves the text it removed when it runs.
- A macro is a command made of commands: steps run in order and are undone in reverse.
References
- Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley (Pearson))
- Interface Runnable (Java SE 25 API) (Oracle)
- The Java Language Specification, Java SE 25: 15.27.2 Lambda Body (Oracle)
- functools: partial (Python Software Foundation)
- Go 1.22 Release Notes: changes to the language (The Go Authors)
- Fixing For Loops in Go 1.22 (The Go Authors (David Chase and Russ Cox))
- ECMAScript Language Specification: CreatePerIterationEnvironment (Ecma International (TC39))
- The Rust Programming Language: Closures (The Rust Project)
- Node.js v24 documentation: Modules: TypeScript (OpenJS Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress