11 KiB
11 KiB
Command Pattern
Problem
A text editor needs to support typing, deleting, and undo/redo operations. If the editor directly performs each operation inline, there's no way to reverse actions, queue them, or log them for replay. Adding undo requires storing inverse operations, which becomes fragile when tightly coupled to the editor logic.
Solution
Encapsulate each operation as a Command object with execute() and undo() methods. The editor (Invoker) stores a history of executed commands and can walk backward (undo) or forward (redo) through that history. The command object holds all the information needed to perform — and reverse — an action.
Key participants:
- Command — interface with
execute()andundo() - ConcreteCommand — implements a specific operation (TypeCommand, DeleteCommand)
- Receiver — the object being operated on (the text buffer)
- Invoker — stores history and triggers execute/undo/redo
When to Use
- You need undo/redo functionality
- Operations should be queued, logged, or scheduled for later execution
- You want to decouple the object that invokes an operation from the one that performs it
- You need to support transactional behavior (execute all or rollback)
When to Avoid
- Operations are simple and don't need undo — adds unnecessary complexity
- The number of command types is huge and each is trivial
- Performance is critical and command object allocation is a bottleneck
Pseudocode
INTERFACE Command:
method execute()
method undo()
CLASS TypeCommand IMPLEMENTS Command:
receiver, text
method execute():
receiver.insert(text)
method undo():
receiver.delete_last(len(text))
CLASS Editor:
history = []
redo_stack = []
method execute_command(cmd):
cmd.execute()
history.push(cmd)
redo_stack.clear()
method undo():
cmd = history.pop()
cmd.undo()
redo_stack.push(cmd)
method redo():
cmd = redo_stack.pop()
cmd.execute()
history.push(cmd)
Python
from __future__ import annotations
from abc import ABC, abstractmethod
class TextBuffer:
def __init__(self) -> None:
self.content = ""
def insert(self, text: str) -> None:
self.content += text
def delete_last(self, count: int) -> str:
removed = self.content[-count:]
self.content = self.content[:-count]
return removed
def __str__(self) -> str:
return self.content
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
class TypeCommand(Command):
def __init__(self, buffer: TextBuffer, text: str) -> None:
self.buffer = buffer
self.text = text
def execute(self) -> None:
self.buffer.insert(self.text)
def undo(self) -> None:
self.buffer.delete_last(len(self.text))
class DeleteCommand(Command):
def __init__(self, buffer: TextBuffer, count: int) -> None:
self.buffer = buffer
self.count = count
self.deleted = ""
def execute(self) -> None:
self.deleted = self.buffer.delete_last(self.count)
def undo(self) -> None:
self.buffer.insert(self.deleted)
class Editor:
def __init__(self) -> None:
self.buffer = TextBuffer()
self.history: list[Command] = []
self.redo_stack: list[Command] = []
def execute(self, cmd: Command) -> None:
cmd.execute()
self.history.append(cmd)
self.redo_stack.clear()
def undo(self) -> None:
if not self.history:
print(" Nothing to undo")
return
cmd = self.history.pop()
cmd.undo()
self.redo_stack.append(cmd)
def redo(self) -> None:
if not self.redo_stack:
print(" Nothing to redo")
return
cmd = self.redo_stack.pop()
cmd.execute()
self.history.append(cmd)
def main() -> None:
editor = Editor()
editor.execute(TypeCommand(editor.buffer, "Hello"))
print(f"After type 'Hello': '{editor.buffer}'")
editor.execute(TypeCommand(editor.buffer, " World"))
print(f"After type ' World': '{editor.buffer}'")
editor.execute(DeleteCommand(editor.buffer, 6))
print(f"After delete 6 chars: '{editor.buffer}'")
editor.undo()
print(f"After undo (restore delete): '{editor.buffer}'")
editor.undo()
print(f"After undo (restore ' World'): '{editor.buffer}'")
editor.redo()
print(f"After redo (re-type ' World'): '{editor.buffer}'")
editor.undo()
print(f"After undo again: '{editor.buffer}'")
editor.undo()
print(f"After undo (restore 'Hello'): '{editor.buffer}'")
if __name__ == "__main__":
main()
Output:
After type 'Hello': 'Hello'
After type ' World': 'Hello World'
After delete 6 chars: 'Hello'
After undo (restore delete): 'Hello World'
After undo (restore ' World'): 'Hello'
After redo (re-type ' World'): 'Hello World'
After undo again: 'Hello'
After undo (restore 'Hello'): ''
Go
package main
import "fmt"
type TextBuffer struct {
content string
}
func (b *TextBuffer) Insert(text string) {
b.content += text
}
func (b *TextBuffer) DeleteLast(count int) string {
if count > len(b.content) {
count = len(b.content)
}
removed := b.content[len(b.content)-count:]
b.content = b.content[:len(b.content)-count]
return removed
}
func (b *TextBuffer) String() string {
return b.content
}
type Command interface {
Execute()
Undo()
}
type TypeCommand struct {
buffer *TextBuffer
text string
}
func (c *TypeCommand) Execute() { c.buffer.Insert(c.text) }
func (c *TypeCommand) Undo() { c.buffer.DeleteLast(len(c.text)) }
type DeleteCommand struct {
buffer *TextBuffer
count int
deleted string
}
func (c *DeleteCommand) Execute() {
c.deleted = c.buffer.DeleteLast(c.count)
}
func (c *DeleteCommand) Undo() {
c.buffer.Insert(c.deleted)
}
type Editor struct {
Buffer *TextBuffer
history []Command
redoStack []Command
}
func NewEditor() *Editor {
return &Editor{Buffer: &TextBuffer{}}
}
func (e *Editor) Execute(cmd Command) {
cmd.Execute()
e.history = append(e.history, cmd)
e.redoStack = nil
}
func (e *Editor) Undo() {
if len(e.history) == 0 {
fmt.Println(" Nothing to undo")
return
}
cmd := e.history[len(e.history)-1]
e.history = e.history[:len(e.history)-1]
cmd.Undo()
e.redoStack = append(e.redoStack, cmd)
}
func (e *Editor) Redo() {
if len(e.redoStack) == 0 {
fmt.Println(" Nothing to redo")
return
}
cmd := e.redoStack[len(e.redoStack)-1]
e.redoStack = e.redoStack[:len(e.redoStack)-1]
cmd.Execute()
e.history = append(e.history, cmd)
}
func main() {
editor := NewEditor()
editor.Execute(&TypeCommand{buffer: editor.Buffer, text: "Hello"})
fmt.Printf("After type 'Hello': '%s'\n", editor.Buffer)
editor.Execute(&TypeCommand{buffer: editor.Buffer, text: " World"})
fmt.Printf("After type ' World': '%s'\n", editor.Buffer)
editor.Execute(&DeleteCommand{buffer: editor.Buffer, count: 6})
fmt.Printf("After delete 6 chars: '%s'\n", editor.Buffer)
editor.Undo()
fmt.Printf("After undo (restore delete): '%s'\n", editor.Buffer)
editor.Undo()
fmt.Printf("After undo (restore ' World'): '%s'\n", editor.Buffer)
editor.Redo()
fmt.Printf("After redo (re-type ' World'): '%s'\n", editor.Buffer)
editor.Undo()
fmt.Printf("After undo again: '%s'\n", editor.Buffer)
editor.Undo()
fmt.Printf("After undo (restore 'Hello'): '%s'\n", editor.Buffer)
}
Output:
After type 'Hello': 'Hello'
After type ' World': 'Hello World'
After delete 6 chars: 'Hello'
After undo (restore delete): 'Hello World'
After undo (restore ' World'): 'Hello'
After redo (re-type ' World'): 'Hello World'
After undo again: 'Hello'
After undo (restore 'Hello'): ''
JavaScript
class TextBuffer {
constructor() {
this.content = "";
}
insert(text) {
this.content += text;
}
deleteLast(count) {
const removed = this.content.slice(-count);
this.content = this.content.slice(0, -count);
return removed;
}
toString() {
return this.content;
}
}
class TypeCommand {
constructor(buffer, text) {
this.buffer = buffer;
this.text = text;
}
execute() {
this.buffer.insert(this.text);
}
undo() {
this.buffer.deleteLast(this.text.length);
}
}
class DeleteCommand {
constructor(buffer, count) {
this.buffer = buffer;
this.count = count;
this.deleted = "";
}
execute() {
this.deleted = this.buffer.deleteLast(this.count);
}
undo() {
this.buffer.insert(this.deleted);
}
}
class Editor {
constructor() {
this.buffer = new TextBuffer();
this.history = [];
this.redoStack = [];
}
execute(cmd) {
cmd.execute();
this.history.push(cmd);
this.redoStack = [];
}
undo() {
if (this.history.length === 0) {
console.log(" Nothing to undo");
return;
}
const cmd = this.history.pop();
cmd.undo();
this.redoStack.push(cmd);
}
redo() {
if (this.redoStack.length === 0) {
console.log(" Nothing to redo");
return;
}
const cmd = this.redoStack.pop();
cmd.execute();
this.history.push(cmd);
}
}
// --- Demo ---
const editor = new Editor();
editor.execute(new TypeCommand(editor.buffer, "Hello"));
console.log(`After type 'Hello': '${editor.buffer}'`);
editor.execute(new TypeCommand(editor.buffer, " World"));
console.log(`After type ' World': '${editor.buffer}'`);
editor.execute(new DeleteCommand(editor.buffer, 6));
console.log(`After delete 6 chars: '${editor.buffer}'`);
editor.undo();
console.log(`After undo (restore delete): '${editor.buffer}'`);
editor.undo();
console.log(`After undo (restore ' World'): '${editor.buffer}'`);
editor.redo();
console.log(`After redo (re-type ' World'): '${editor.buffer}'`);
editor.undo();
console.log(`After undo again: '${editor.buffer}'`);
editor.undo();
console.log(`After undo (restore 'Hello'): '${editor.buffer}'`);
Output:
After type 'Hello': 'Hello'
After type ' World': 'Hello World'
After delete 6 chars: 'Hello'
After undo (restore delete): 'Hello World'
After undo (restore ' World'): 'Hello'
After redo (re-type ' World'): 'Hello World'
After undo again: 'Hello'
After undo (restore 'Hello'): ''
Related Patterns
- Memento — Command stores what action was taken; Memento stores the state before the action. Together they enable undo/redo.
- Strategy — Both encapsulate behavior as objects. Commands are action-oriented (do/undo); Strategies are algorithm-oriented (how to do something).
- Chain of Responsibility — Commands can be passed along a chain; each handler decides to execute or forward.
- Composite — Macro commands are Composite Commands: a composite that executes a sequence of child commands atomically.
- Observer — Commands are often triggered by Observer events and can emit events on completion.
- Message Queue — Commands are natural messages; queuing them decouples invocation from execution.