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 { private String text = ""; private String lastAction = ""; private String lastText = "";
public void type(String s) { text += s; lastAction = "type"; lastText = s; }
public void deleteLast(int count) { lastText = text.substring(text.length() - count); text = text.substring(0, text.length() - count); lastAction = "delete"; }
public void undo() { if (lastAction.equals("type")) { text = text.substring(0, text.length() - lastText.length()); } else if (lastAction.equals("delete")) { text += lastText; } lastAction = ""; }
public String text() { return text; }}Editor editor = new Editor();editor.type("Buy");editor.type(" milk");editor.undo();System.out.println(editor.text());editor.undo();System.out.println(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. removeLast returns the deleted text:
class Document { private String text = "";
public void append(String s) { text += s; }
public String removeLast(int count) { String removed = text.substring(text.length() - count); text = text.substring(0, text.length() - count); return removed; }
public String text() { return text; }}The common shape of every command:
interface Command { void execute(); void undo();}A command for typing. It keeps the document and its text, so it knows how much to delete on undo:
class TypeText implements Command { private final Document doc; private final String text;
public TypeText(Document doc, String text) { this.doc = doc; this.text = text; }
@Override public void execute() { doc.append(text); }
@Override public void undo() { doc.removeLast(text.length()); }}The history doesn't know what the commands are. It only calls them and moves them between the stacks:
class History { private final Deque<Command> undoStack = new ArrayDeque<>(); private final Deque<Command> redoStack = new ArrayDeque<>();
public void run(Command command) { command.execute(); undoStack.push(command); redoStack.clear(); }
public void undo() { if (undoStack.isEmpty()) { return; } Command command = undoStack.pop(); command.undo(); redoStack.push(command); }
public void redo() { if (redoStack.isEmpty()) { return; } Command command = redoStack.pop(); command.execute(); undoStack.push(command); }}Document doc = new Document();History history = new History();history.run(new TypeText(doc, "Buy"));history.run(new TypeText(doc, " milk"));history.run(new TypeText(doc, " and bread"));history.undo();history.undo();history.redo();history.run(new TypeText(doc, " and eggs"));System.out.println(doc.text());Buy milk and eggsDocument doc = new Document();History history = new History();history.run(new TypeText(doc, "Buy"));history.run(new TypeText(doc, " milk"));history.run(new TypeText(doc, " and bread"));history.undo();history.undo();history.redo();history.run(new TypeText(doc, " and eggs"));System.out.println(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 redoStack.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 implements Command { private final Document doc; private final int count; private String removed = "";
public DeleteLast(Document doc, int count) { this.doc = doc; this.count = count; }
@Override public void execute() { removed = doc.removeLast(count); }
@Override public void undo() { doc.append(removed); }}Document doc = new Document();History history = new History();history.run(new TypeText(doc, "Buy milk"));history.run(new DeleteLast(doc, 5));System.out.println(doc.text());history.undo();System.out.println(doc.text());history.redo();System.out.println(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:
public void undo() { Command command = undoStack.pop(); command.undo(); redoStack.push(command);}History history = new History();history.undo();Exception in thread "main" java.util.NoSuchElementExceptionpop() on an empty ArrayDeque throws NoSuchElementException. 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
Command has two methods, so it isn't a functional interface, and a lambda won't do. An anonymous class does the same as two lambdas: it sees doc and text from the method that creates it, and History doesn't notice the difference:static Command typeText(Document doc, String text) { return new Command() { @Override public void execute() { doc.append(text); }
@Override public void undo() { doc.removeLast(text.length()); } };}Document doc = new Document();History history = new History();history.run(typeText(doc, "Buy"));history.run(typeText(doc, " milk"));history.undo();System.out.println(doc.text());history.redo();System.out.println(doc.text());BuyBuy milkThe lambdas close over (remember) doc and text, just as the fields of the class kept them. In Python and JavaScript this is the usual way when the command is short: the command is just two functions.
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?
Document doc = new Document();History history = new History();history.run(new TypeText(doc, "Meeting"));history.run(new TypeText(doc, " at 10:00"));history.undo();history.run(new TypeText(doc, " at 11:00"));history.redo();history.undo();System.out.println(doc.text());Try it
Find the bug
Someone writes the history again:
class History { private final Deque<Command> undoStack = new ArrayDeque<>(); private final Deque<Command> redoStack = new ArrayDeque<>();
public void run(Command command) { command.execute(); undoStack.push(command); }
public void undo() { if (undoStack.isEmpty()) { return; } Command command = undoStack.pop(); command.undo(); redoStack.push(command); }
public void redo() { if (redoStack.isEmpty()) { return; } Command command = redoStack.pop(); command.execute(); undoStack.push(command); }}Document doc = new Document();History history = new History();history.run(new TypeText(doc, "Buy"));history.run(new TypeText(doc, " milk"));history.undo();history.run(new TypeText(doc, " bread"));history.redo();System.out.println(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 redoStack.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 · Lambdas · Design patterns · Composite
- Continues in: The shop: which pattern where
- Tasks on this topic: Design patterns