Command
In short: Command turns every action into an object with execute and undo, so actions are kept in a history and can be undone and redone.
Problem
An undo that remembers only one thing
The small notes editor types text and deletes from the end. Undo is done like this: the editor keeps, as text, what the last action was and what text was involved.
class Editor: def __init__(self): self.text = "" self.last_action = "" self.last_text = ""
def type(self, s): self.text += s self.last_action = "type" self.last_text = s
def delete_last(self, count): self.last_text = self.text[-count:] self.text = self.text[:-count] self.last_action = "delete"
def undo(self): if self.last_action == "type": self.text = self.text[: -len(self.last_text)] elif self.last_action == "delete": self.text += self.last_text self.last_action = ""editor = Editor()editor.type("Buy")editor.type(" milk")editor.undo()print(editor.text)editor.undo()print(editor.text)BuyBuyThe first undo removed " milk". The second did nothing: "Buy" stayed, because the editor remembers only one action, and after the undo even that one is gone. There is no redo at all: the text that undo removed is lost.
Every new action (bold, replace, paste) is one more branch in undo() and more fields in the editor. The knowledge of how to undo an action lives far from the action itself.
The idea
Command: the action as an object
Instead of "the last action was type", every action becomes an object that knows two things: how to run (execute()) and how to go back (undo()). Whatever it needs to go back, it keeps in its own fields.
The history keeps the commands on two stacks:
- the undo stack: the commands that ran. Undo takes the top one, calls
undo()and puts it on the redo stack; - the redo stack: the commands that were undone. Redo takes the top one, runs it again and puts it back on the undo stack;
- a new command clears the redo stack. The undone commands were made on a different text, and there is nowhere to repeat them.
This is the Command pattern. There are three roles: the command (the action as an object), the receiver (the document it works on) and the history, which calls the commands without knowing what they do.
See it
Two stacks
The document only knows how to append and how to delete from the end. remove_last returns the deleted text:
class Document: def __init__(self): self.text = ""
def append(self, s): self.text += s
def remove_last(self, count): removed = self.text[-count:] self.text = self.text[:-count] return removedA command for typing. It keeps the document and its text, so it knows how much to delete on undo:
class TypeText: def __init__(self, doc, text): self.doc = doc self.text = text
def execute(self): self.doc.append(self.text)
def undo(self): self.doc.remove_last(len(self.text))The history doesn't know what the commands are. It only calls them and moves them between the stacks:
class History: def __init__(self): self.undo_stack = [] self.redo_stack = []
def run(self, command): command.execute() self.undo_stack.append(command) self.redo_stack.clear()
def undo(self): if not self.undo_stack: return command = self.undo_stack.pop() command.undo() self.redo_stack.append(command)
def redo(self): if not self.redo_stack: return command = self.redo_stack.pop() command.execute() self.undo_stack.append(command)doc = Document()history = History()history.run(TypeText(doc, "Buy"))history.run(TypeText(doc, " milk"))history.run(TypeText(doc, " and bread"))history.undo()history.undo()history.redo()history.run(TypeText(doc, " and eggs"))print(doc.text)Buy milk and eggsdoc = Document()history = History()history.run(TypeText(doc, "Buy"))history.run(TypeText(doc, " milk"))history.run(TypeText(doc, " and bread"))history.undo()history.undo()history.redo()history.run(TypeText(doc, " and eggs"))print(doc.text)Step by step, in words
- run calls execute() on the command: the document gets "Buy". Then the command goes onto the undo stack.
- A second command, on top of the first. The top of the stack is always the last one that ran.
- A third one. The undo stack remembers the order of everything that happened.
- undo takes the top command and calls its undo(): " and bread" is removed. The command moves to the redo stack.
- One more undo: " milk" goes too. You can go back as many steps as there are commands.
- redo takes the top of the redo stack and calls execute() again: " milk" is back. The command returns to the undo stack.
- A new command. " and bread" was undone on a different text, so redo_stack.clear() empties the redo stack.
- The document is "Buy milk and eggs". That is what the program prints.
Undo over as many steps as you like and redo come from two stacks and three short methods. A new kind of action is a new class, and the history isn't touched.
See it
A command that remembers
Deleting can't be undone by the number of letters: you have to know what was deleted. The command remembers it in execute():
class DeleteLast: def __init__(self, doc, count): self.doc = doc self.count = count self.removed = ""
def execute(self): self.removed = self.doc.remove_last(self.count)
def undo(self): self.doc.append(self.removed)doc = Document()history = History()history.run(TypeText(doc, "Buy milk"))history.run(DeleteLast(doc, 5))print(doc.text)history.undo()print(doc.text)history.redo()print(doc.text)BuyBuy milkBuyNot a single line of History or TypeText changed.
The check at the start of undo() isn't decoration. Without it, an undo in an empty history stops the program:
def undo(self): command = self.undo_stack.pop() command.undo() self.redo_stack.append(command)history = History()history.undo()IndexError: pop from empty listpop() on an empty list raises IndexError. People press Ctrl+Z more times than they typed, and that is normal, so an empty stack means "nothing to undo".
See it
A command made of two functions
namedtuple) of two lambdas. History calls command.execute() and command.undo() and doesn't notice the difference:from collections import namedtuple
Command = namedtuple("Command", ["execute", "undo"])
def type_text(doc, text): return Command( execute=lambda: doc.append(text), undo=lambda: doc.remove_last(len(text)), )doc = Document()history = History()history.run(type_text(doc, "Buy"))history.run(type_text(doc, " milk"))history.undo()print(doc.text)history.redo()print(doc.text)BuyBuy milkThe lambdas close over (remember) doc and text, just as the fields of the class kept them. This is the usual way in Python when the command is short. In Java the same is done with an anonymous class, and in C# with two Actions.
A class pays off when the command remembers something that changes between execute() and undo(), like the deleted text. Functions can do that too, but the state hides in the closure and is harder to read.
Try it
Predict the output
A note about a meeting is typed, undone and typed again. What does the program print?
doc = Document()history = History()history.run(TypeText(doc, "Meeting"))history.run(TypeText(doc, " at 10:00"))history.undo()history.run(TypeText(doc, " at 11:00"))history.redo()history.undo()print(doc.text)Try it
Find the bug
Someone writes the history again:
class History: def __init__(self): self.undo_stack = [] self.redo_stack = []
def run(self, command): command.execute() self.undo_stack.append(command)
def undo(self): if not self.undo_stack: return command = self.undo_stack.pop() command.undo() self.redo_stack.append(command)
def redo(self): if not self.redo_stack: return command = self.redo_stack.pop() command.execute() self.undo_stack.append(command)doc = Document()history = History()history.run(TypeText(doc, "Buy"))history.run(TypeText(doc, " milk"))history.undo()history.run(TypeText(doc, " bread"))history.redo()print(doc.text)Buy bread milkRedo put " milk" into a note where " milk" had been undone before " bread" was typed.
Pick the line that should have been followed by one more.
run puts the new command on the undo stack, but doesn't clear the redo stack. " milk" stays there, and redo repeats it on a text where it no longer fits. With more complex commands it is even worse: a delete repeated on a different text deletes the wrong letters. After this line there has to be self.redo_stack.clear(). Then redo after a new command does nothing, and the note is "Buy bread".
The idea
When you don't need it
Command has a cost: a class (or two functions) for every action, instead of calling the method directly. It isn't worth it when:
- the action is called right away, there is no undo, it isn't queued and it isn't recorded. Then the command is a wrapper around one call;
- the state is small. If the document is a short text, the history can keep copies of the text before every action, and undo is "go back to the previous copy". That is simpler and can't get the reverse action wrong;
- undo is needed for only one step, in one place.
It is worth it when actions have to be undone over many steps, repeated, wait in a queue (jobs to send), be recorded and played again (a macro) or run later.
Exercise
Upper case
Solve the task in your editor.
Upper case. Add a command Upper that makes the whole text upper case, and whose undo brings the text back the way it was. Document and History are not touched. Type "buy milk", run Upper, then undo and redo, and print the text after each of the three steps:
BUY MILKbuy milkBUY MILKMore tasks
- A macro. A command that holds a list of commands:
execute()runs them in order, andundo()undoes them in reverse order. This is a Composite of commands. Why is the order reversed on undo? - Only 3 steps back. The history remembers at most 3 commands for undo. The oldest one is thrown away.
- The cart. Commands "add a product" and "remove a product" for a shop cart, with undo and redo. What does "remove" have to remember?
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Nested classes and data · Lambdas · Design patterns · Composite
- Continues in: The shop: which pattern where
- Tasks on this topic: Design patterns