Command Pattern: Undo/Redo, Logging, and Transactions + Interpreter Pattern
This document covers the second half of the Command pattern (implementation details, undo/redo mechanics, sample code) and the complete Interpreter pattern from the Gang of Four. Command focuses on encapsulating requests as objects to support undo, logging, and macro composition. Interpreter focuses on representing a grammar as a class hierarchy and evaluating sentences in that grammar via recursive interpretation.
Key Concepts
Command Pattern (continued):
Participants: Command (interface), ConcreteCommand (binds receiver + action), Client (creates commands), Invoker (triggers execution), Receiver (performs the actual work)
Decoupling: Command decouples the invoker from the receiver — the invoker doesn't know what object performs the operation or how
Commands are first-class objects — they can be stored, passed, composed, serialized, and extended like any other object
Composite commands (MacroCommand): A command that holds a list of sub-commands and executes them sequentially — an instance of the Composite pattern
Open/Closed: Easy to add new commands without changing existing classes
Command intelligence spectrum: From thin (just a receiver-action binding) to fat (implements everything itself, no receiver needed). Middle ground: commands that find their receiver dynamically
Undo/Redo mechanism: ConcreteCommand stores state before Execute; a history list of executed commands enables traversal backward (Unexecute) and forward (Execute)
Copy-before-store: Undoable commands may need to be copied before placement on the history list if their state varies across invocations (acts as Prototype pattern)
Error accumulation (hysteresis): Repeated undo/redo can cause state drift; use Memento pattern to store/restore exact state without exposing internals
C++ templates for simple commands: SimpleCommand<Receiver> parameterized by receiver type and member function pointer — avoids subclass explosion for non-undoable, argument-free commands
Logging for crash recovery: Augment Command with load/store operations; persist commands to disk; replay on recovery
Transaction modeling: Commands with a common interface can model transactions uniformly
Interpreter Pattern:
Intent: Define a representation for a grammar and an interpreter that uses it to interpret sentences
When to use: The problem recurs often enough to warrant a mini-language; the grammar is simple; efficiency is not critical
One class per grammar rule: Each rule in the grammar maps to a class; symbols on the right-hand side become instance variables
Abstract Syntax Tree (AST): Sentences are represented as ASTs composed of TerminalExpression and NonterminalExpression instances
Recursive interpretation: NonterminalExpression.Interpret calls Interpret on its sub-expressions; TerminalExpression defines the base case
Context object: Holds global state for the interpreter (e.g., the input string and match progress)
Easy to extend grammar via inheritance — new expressions are variations on old ones
Complex grammars become unmanageable — use parser/compiler generators instead
Adding new interpretations: Define a new Interpret-like operation on expression classes, or use the Visitor pattern to avoid modifying grammar classes
Commands and Syntax
Command Pattern — MacroCommand (C++):
class MacroCommand : public Command {
public:
virtual void Execute();
virtual void Add(Command*);
virtual void Remove(Command*);
private:
List<Command*>* _cmds;
};
void MacroCommand::Execute() {
ListIterator<Command*> i(_cmds);
for (i.First(); !i.IsDone(); i.Next()) {
Command* c = i.CurrentItem();
c->Execute();
}
}
// Unexecute must reverse the order
Composite (163) — MacroCommand is a composite of commands
Memento (283) — Stores state needed for reliable undo without exposing receiver internals
Prototype (117) — Commands copied before history-list insertion act as prototypes
Chain of Responsibility (223) — THINK library passes Task (command) objects along a chain for consumption
Interpreter pattern connections:
Composite (163) — Interpreter and Composite share implementation issues; the AST is inherently a composite structure
Visitor (331) — When many operations are needed on the AST (type-checking, optimization, code generation), put Interpret in a Visitor to avoid modifying grammar classes
Flyweight (195) — Share terminal symbol instances when they appear many times (e.g., variable references in code); parent nodes pass extrinsic context during interpretation
Iterator — Implied by tree traversal of the AST
Exam-Relevant Points
Command's five participants and their roles: Command, ConcreteCommand, Client, Invoker, Receiver — know which creates, which stores, which executes
Undo requires storing state before Execute: The ConcreteCommand stores receiver, arguments, and original values
History list direction: Backward traversal = Unexecute (undo); forward traversal = Execute (redo)
Commands must be copied for history if their state varies across invocations; if state is constant, a reference suffices
Hysteresis problem: Repeated undo/redo can accumulate errors; Memento pattern mitigates this
MacroCommand Unexecute must reverse sub-command order relative to Execute
Interpreter maps one class per grammar rule — TerminalExpression for terminals, NonterminalExpression for non-terminals
Interpreter works best with simple grammars — for complex grammars, use parser generators
Flyweight optimization for Interpreter: Terminal symbols shared across AST; intrinsic state (the symbol) vs. extrinsic state (context passed in during interpretation)
Interpreter doesn't address parsing — AST construction is a separate concern (table-driven parser, recursive descent, or direct client construction)
Visitor is the recommended extension when many operations are needed on expression classes, avoiding modification of the grammar hierarchy