Skip to content
wannacode

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)
Output
Buy
Buy

The 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 removed

A 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)
Output
Buy milk and eggs

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)
Step by step, in words
  1. run calls execute() on the command: the document gets "Buy". Then the command goes onto the undo stack.
  2. A second command, on top of the first. The top of the stack is always the last one that ran.
  3. A third one. The undo stack remembers the order of everything that happened.
  4. undo takes the top command and calls its undo(): " and bread" is removed. The command moves to the redo stack.
  5. One more undo: " milk" goes too. You can go back as many steps as there are commands.
  6. redo takes the top of the redo stack and calls execute() again: " milk" is back. The command returns to the undo stack.
  7. A new command. " and bread" was undone on a different text, so redo_stack.clear() empties the redo stack.
  8. 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)
Output
Buy
Buy milk
Buy

Not 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()
Error
IndexError: pop from empty list

pop() 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

The typing command has two actions and nothing else. In Python those are two functions: the command can be a named tuple (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)
Output
Buy
Buy milk

The 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)
Answer

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)
Output
Buy bread milk

Redo 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.

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:

Output
BUY MILK
buy milk
BUY MILK

More tasks

  • A macro. A command that holds a list of commands: execute() runs them in order, and undo() 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

1. What does the history keep with Command?
2. You type "Buy milk", undo " milk" and type " bread". What should happen to the redo stack?
3. Why does the delete command keep the deleted text in a field?
4. When don't you need Command?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·