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 lastAction = ""; private string lastText = "";
public string Text { get; private set; } = "";
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 == "type") { Text = Text.Substring(0, Text.Length - lastText.Length); } else if (lastAction == "delete") { Text += lastText; } lastAction = ""; }}var editor = new Editor();editor.Type("Buy");editor.Type(" milk");editor.Undo();Console.WriteLine(editor.Text);editor.Undo();Console.WriteLine(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{ public string Text { get; private set; } = "";
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; }}The common shape of every command:
interface ICommand{ 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 : ICommand{ private readonly Document doc; private readonly string text;
public TypeText(Document doc, string text) { this.doc = doc; this.text = text; }
public void Execute() => doc.Append(text); 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 readonly Stack<ICommand> undoStack = new(); private readonly Stack<ICommand> redoStack = new();
public void Run(ICommand command) { command.Execute(); undoStack.Push(command); redoStack.Clear(); }
public void Undo() { if (undoStack.Count == 0) { return; } ICommand command = undoStack.Pop(); command.Undo(); redoStack.Push(command); }
public void Redo() { if (redoStack.Count == 0) { return; } ICommand command = redoStack.Pop(); command.Execute(); undoStack.Push(command); }}var doc = new Document();var 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"));Console.WriteLine(doc.Text);Buy milk and eggsvar doc = new Document();var 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"));Console.WriteLine(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 : ICommand{ private readonly Document doc; private readonly int count; private string removed = "";
public DeleteLast(Document doc, int count) { this.doc = doc; this.count = count; }
public void Execute() => removed = doc.RemoveLast(count); public void Undo() => doc.Append(removed);}var doc = new Document();var history = new History();history.Run(new TypeText(doc, "Buy milk"));history.Run(new DeleteLast(doc, 5));Console.WriteLine(doc.Text);history.Undo();Console.WriteLine(doc.Text);history.Redo();Console.WriteLine(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(){ ICommand command = undoStack.Pop(); command.Undo(); redoStack.Push(command);}var history = new History();history.Undo();Unhandled exception. System.InvalidOperationException: Stack empty.Pop() on an empty Stack throws InvalidOperationException. 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
Action delegates: one shared class ActionCommand keeps them, and every command is two lambdas. History doesn't notice the difference:class ActionCommand : ICommand{ private readonly Action execute; private readonly Action undo;
public ActionCommand(Action execute, Action undo) { this.execute = execute; this.undo = undo; }
public void Execute() => execute(); public void Undo() => undo();}static ICommand TypeText(Document doc, string text) => new ActionCommand(() => doc.Append(text), () => doc.RemoveLast(text.Length));var doc = new Document();var history = new History();history.Run(TypeText(doc, "Buy"));history.Run(TypeText(doc, " milk"));history.Undo();Console.WriteLine(doc.Text);history.Redo();Console.WriteLine(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?
var doc = new Document();var 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();Console.WriteLine(doc.Text);Try it
Find the bug
Someone writes the history again:
class History{ private readonly Stack<ICommand> undoStack = new(); private readonly Stack<ICommand> redoStack = new();
public void Run(ICommand command) { command.Execute(); undoStack.Push(command); }
public void Undo() { if (undoStack.Count == 0) { return; } ICommand command = undoStack.Pop(); command.Undo(); redoStack.Push(command); }
public void Redo() { if (redoStack.Count == 0) { return; } ICommand command = redoStack.Pop(); command.Execute(); undoStack.Push(command); }}var doc = new Document();var 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();Console.WriteLine(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 and delegates · Design patterns · Composite
- Continues in: The shop: which pattern where
- Tasks on this topic: Design patterns