Design Principle
Command Pattern
Learn the Command pattern: encapsulate a request as an object, allowing you to parameterize clients with different requests, queue operations, and support undo.
The Command Pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue operations, log requests, and support undo operations.
What is the Command Pattern?
Command Pattern provides:
- Request encapsulation: Encapsulate request as object
- Decoupling: Decouple invoker from receiver
- Undo/Redo: Support undo and redo operations
- Queue operations: Queue and schedule operations
- Logging: Log and audit operations
Use cases:
- Undo/redo functionality
- Macro recording
- Transaction systems
- Job queues
- Remote procedure calls
Structure
Command (interface)
└─ execute()
└─ undo()
ConcreteCommand (implements Command)
└─ receiver: Receiver
└─ execute() (calls receiver.action())
└─ undo() (reverses action)
Invoker
└─ command: Command
└─ execute() (calls command.execute())
Receiver
└─ action()
Examples
Basic Command Pattern
// Command interface
interface Command {
execute(): void;
undo(): void;
}
// Receiver
class Light {
private isOn: boolean = false;
turnOn(): void {
this.isOn = true;
console.log("Light is ON");
}
turnOff(): void {
this.isOn = false;
console.log("Light is OFF");
}
getState(): boolean {
return this.isOn;
}
}
// Concrete commands
class TurnOnCommand implements Command {
private light: Light;
constructor(light: Light) {
this.light = light;
}
execute(): void {
this.light.turnOn();
}
undo(): void {
this.light.turnOff();
}
}
class TurnOffCommand implements Command {
private light: Light;
constructor(light: Light) {
this.light = light;
}
execute(): void {
this.light.turnOff();
}
undo(): void {
this.light.turnOn();
}
}
// Invoker
class RemoteControl {
private command: Command | null = null;
private history: Command[] = [];
setCommand(command: Command): void {
this.command = command;
}
pressButton(): void {
if (this.command) {
this.command.execute();
this.history.push(this.command);
}
}
undo(): void {
if (this.history.length > 0) {
const lastCommand = this.history.pop()!;
lastCommand.undo();
}
}
}
// Usage
const light = new Light();
const turnOn = new TurnOnCommand(light);
const turnOff = new TurnOffCommand(light);
const remote = new RemoteControl();
remote.setCommand(turnOn);
remote.pressButton(); // Light is ON
remote.setCommand(turnOff);
remote.pressButton(); // Light is OFF
remote.undo(); // Light is ON (undo last command)
Text Editor with Undo/Redo
// Command interface
interface TextCommand {
execute(): void;
undo(): void;
}
// Receiver
class TextEditor {
private content: string = "";
insert(text: string, position: number): void {
this.content = this.content.slice(0, position) + text + this.content.slice(position);
}
delete(position: number, length: number): string {
const deleted = this.content.slice(position, position + length);
this.content = this.content.slice(0, position) + this.content.slice(position + length);
return deleted;
}
getContent(): string {
return this.content;
}
}
// Concrete commands
class InsertCommand implements TextCommand {
private editor: TextEditor;
private text: string;
private position: number;
constructor(editor: TextEditor, text: string, position: number) {
this.editor = editor;
this.text = text;
this.position = position;
}
execute(): void {
this.editor.insert(this.text, this.position);
}
undo(): void {
this.editor.delete(this.position, this.text.length);
}
}
class DeleteCommand implements TextCommand {
private editor: TextEditor;
private position: number;
private length: number;
private deletedText: string = "";
constructor(editor: TextEditor, position: number, length: number) {
this.editor = editor;
this.position = position;
this.length = length;
}
execute(): void {
this.deletedText = this.editor.delete(this.position, this.length);
}
undo(): void {
this.editor.insert(this.deletedText, this.position);
}
}
// Command history
class CommandHistory {
private history: TextCommand[] = [];
private currentIndex: number = -1;
execute(command: TextCommand): void {
// Remove any commands after current index (when undoing and then doing new command)
this.history = this.history.slice(0, this.currentIndex + 1);
command.execute();
this.history.push(command);
this.currentIndex++;
}
undo(): void {
if (this.canUndo()) {
this.history[this.currentIndex].undo();
this.currentIndex--;
}
}
redo(): void {
if (this.canRedo()) {
this.currentIndex++;
this.history[this.currentIndex].execute();
}
}
canUndo(): boolean {
return this.currentIndex >= 0;
}
canRedo(): boolean {
return this.currentIndex < this.history.length - 1;
}
}
// Usage
const editor = new TextEditor();
const history = new CommandHistory();
history.execute(new InsertCommand(editor, "Hello", 0));
history.execute(new InsertCommand(editor, " World", 5));
console.log(editor.getContent()); // "Hello World"
history.undo();
console.log(editor.getContent()); // "Hello"
history.redo();
console.log(editor.getContent()); // "Hello World"
Job Queue
// Command interface
interface Job {
execute(): Promise<void>;
getName(): string;
}
// Concrete commands
class EmailJob implements Job {
constructor(
private to: string,
private subject: string,
private body: string
) {}
async execute(): Promise<void> {
console.log(`Sending email to ${this.to}: ${this.subject}`);
// Simulate email sending
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`Email sent to ${this.to}`);
}
getName(): string {
return `Email to ${this.to}`;
}
}
class DatabaseBackupJob implements Job {
constructor(private database: string) {}
async execute(): Promise<void> {
console.log(`Backing up database: ${this.database}`);
await new Promise(resolve => setTimeout(resolve, 2000));
console.log(`Backup completed for ${this.database}`);
}
getName(): string {
return `Backup ${this.database}`;
}
}
// Invoker (Job Queue)
class JobQueue {
private queue: Job[] = [];
private isProcessing: boolean = false;
addJob(job: Job): void {
this.queue.push(job);
console.log(`Job added: ${job.getName()}`);
this.processQueue();
}
private async processQueue(): Promise<void> {
if (this.isProcessing || this.queue.length === 0) {
return;
}
this.isProcessing = true;
while (this.queue.length > 0) {
const job = this.queue.shift()!;
try {
await job.execute();
} catch (error) {
console.error(`Job failed: ${job.getName()}`, error);
}
}
this.isProcessing = false;
}
getQueueSize(): number {
return this.queue.length;
}
}
// Usage
const queue = new JobQueue();
queue.addJob(new EmailJob("user@example.com", "Welcome", "Welcome to our service!"));
queue.addJob(new DatabaseBackupJob("production"));
queue.addJob(new EmailJob("admin@example.com", "Backup Complete", "Backup finished"));
Common Pitfalls
- Command bloat: Too many command classes. Fix: Use parameterized commands
- Memory usage: History can consume memory. Fix: Limit history size
- Undo complexity: Complex undo logic. Fix: Store state snapshots
- Not handling errors: Commands fail silently. Fix: Add error handling
Interview Questions
Beginner
Q: What is the Command pattern and when would you use it?
A:
Command Pattern encapsulates a request as an object, allowing you to parameterize clients with different requests.
Key characteristics:
- Request encapsulation: Request becomes an object
- Decoupling: Decouples invoker from receiver
- Undo/Redo: Support undo and redo operations
- Queue operations: Queue and schedule operations
Example:
interface Command {
execute(): void;
undo(): void;
}
class LightOnCommand implements Command {
private light: Light;
execute(): void {
this.light.turnOn();
}
undo(): void {
this.light.turnOff();
}
}
Use cases:
- Undo/redo: Text editors, graphics editors
- Macro recording: Record and replay commands
- Job queues: Queue operations for later execution
- Transaction systems: Atomic operations
Benefits:
- Decoupling: Invoker doesn't know receiver
- Undo/redo: Easy to implement
- Queue operations: Schedule commands
- Logging: Log all commands
Intermediate
Q: Explain how the Command pattern supports undo/redo. How do you implement command history?
A:
Command Pattern for Undo/Redo:
1. Command Interface:
interface Command {
execute(): void;
undo(): void;
}
2. Concrete Command:
class InsertCommand implements Command {
private editor: TextEditor;
private text: string;
private position: number;
execute(): void {
this.editor.insert(this.text, this.position);
}
undo(): void {
this.editor.delete(this.position, this.text.length);
}
}
3. Command History:
class CommandHistory {
private history: Command[] = [];
private currentIndex: number = -1;
execute(command: Command): void {
// Remove commands after current (when doing new command after undo)
this.history = this.history.slice(0, this.currentIndex + 1);
command.execute();
this.history.push(command);
this.currentIndex++;
}
undo(): void {
if (this.canUndo()) {
this.history[this.currentIndex].undo();
this.currentIndex--;
}
}
redo(): void {
if (this.canRedo()) {
this.currentIndex++;
this.history[this.currentIndex].execute();
}
}
}
Key Points:
- Store commands: Keep executed commands in history
- Track index: Track current position in history
- Remove future: When doing new command after undo, remove future commands
- Execute/undo: Execute for redo, undo for undo
Senior
Q: Design a command system for a distributed transaction system that supports rollback, logging, and ensures commands are executed atomically across multiple services.
A:
// Command interface
interface TransactionCommand {
execute(): Promise<void>;
rollback(): Promise<void>;
getServiceId(): string;
getId(): string;
}
// Concrete commands
class TransferCommand implements TransactionCommand {
private id: string;
private serviceId: string = "payment-service";
private fromAccount: string;
private toAccount: string;
private amount: number;
private executed: boolean = false;
constructor(fromAccount: string, toAccount: string, amount: number) {
this.id = this.generateId();
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
async execute(): Promise<void> {
// Deduct from source account
await this.deduct(this.fromAccount, this.amount);
// Credit to destination account
await this.credit(this.toAccount, this.amount);
this.executed = true;
}
async rollback(): Promise<void> {
if (this.executed) {
// Reverse the transaction
await this.credit(this.fromAccount, this.amount);
await this.deduct(this.toAccount, this.amount);
this.executed = false;
}
}
getServiceId(): string {
return this.serviceId;
}
getId(): string {
return this.id;
}
private async deduct(account: string, amount: number): Promise<void> {
// Deduct logic
}
private async credit(account: string, amount: number): Promise<void> {
// Credit logic
}
private generateId(): string {
return `cmd-${Date.now()}-${Math.random()}`;
}
}
// Transaction coordinator
class TransactionCoordinator {
private commands: TransactionCommand[] = [];
private executedCommands: TransactionCommand[] = [];
private logger: TransactionLogger;
constructor(logger: TransactionLogger) {
this.logger = logger;
}
addCommand(command: TransactionCommand): void {
this.commands.push(command);
}
async execute(): Promise<void> {
// Two-phase commit
try {
// Phase 1: Prepare (validate all commands)
await this.prepare();
// Phase 2: Commit (execute all commands)
await this.commit();
this.logger.log("Transaction committed", this.commands);
} catch (error) {
// Rollback on failure
await this.rollback();
this.logger.log("Transaction rolled back", this.commands);
throw error;
}
}
private async prepare(): Promise<void> {
// Validate all commands can execute
for (const command of this.commands) {
await this.validateCommand(command);
}
}
private async commit(): Promise<void> {
// Execute all commands
for (const command of this.commands) {
try {
await command.execute();
this.executedCommands.push(command);
} catch (error) {
// If any command fails, rollback all executed
await this.rollbackExecuted();
throw error;
}
}
}
private async rollback(): Promise<void> {
// Rollback in reverse order
for (let i = this.executedCommands.length - 1; i >= 0; i--) {
try {
await this.executedCommands[i].rollback();
} catch (error) {
// Log but continue rollback
this.logger.logError(`Rollback failed for ${this.executedCommands[i].getId()}`, error);
}
}
this.executedCommands = [];
}
private async rollbackExecuted(): Promise<void> {
await this.rollback();
}
private async validateCommand(command: TransactionCommand): Promise<void> {
// Validate command can execute
// Check account exists, sufficient funds, etc.
}
}
// Transaction logger
class TransactionLogger {
log(message: string, commands: TransactionCommand[]): void {
console.log(`${message}:`, commands.map(c => c.getId()));
// Log to persistent storage
}
logError(message: string, error: any): void {
console.error(message, error);
// Log error to persistent storage
}
}
// Usage
const coordinator = new TransactionCoordinator(new TransactionLogger());
coordinator.addCommand(new TransferCommand("acc1", "acc2", 100));
coordinator.addCommand(new TransferCommand("acc2", "acc3", 50));
try {
await coordinator.execute();
console.log("Transaction successful");
} catch (error) {
console.error("Transaction failed:", error);
}
Features:
- Atomic execution: All commands execute or none
- Rollback: Rollback all commands on failure
- Logging: Log all transactions
- Two-phase commit: Prepare then commit
- Error handling: Handle failures gracefully
Key Takeaways
- Command pattern: Encapsulates request as object
- Decoupling: Decouples invoker from receiver
- Undo/redo: Easy to implement with command history
- Queue operations: Queue and schedule commands
- Use cases: Undo/redo, job queues, transactions, macros
- Best practices: Store state for undo, handle errors, limit history size
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.