Command Pattern - Encapsulating Requests as Objects
Learning Objectives: By the end of this lesson, you will be able to:
- Understand the Command pattern and its role in decoupling senders from receivers
- Implement commands with undo/redo functionality in multiple programming languages
- Apply Command pattern to create flexible and extensible systems
- Design macro commands and command queues for complex operations
- Recognize command patterns in frameworks, GUI toolkits, and transaction systems
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Think about your text editor or IDE. When you press Ctrl+Z to undo, or use macros to repeat a series of actions, what’s happening behind the scenes? How does the application remember what you did and reverse it? This is the Command pattern in action!
Lesson Overview
- Duration: 120 minutes
- Prerequisites: Object-oriented programming, basic understanding of interfaces and polymorphism
- Materials Needed: Development environment with Java, Python, or JavaScript support
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Request Encapsulation - Turning method calls into objects
- Command Pattern Structure - Command, Receiver, Invoker, and Client relationships
- Undo/Redo Implementation - Reversible operations and command history
- Advanced Patterns - Macro commands, queuing, logging, and transactions
- Real-world Applications - GUI actions, database transactions, batch processing
- Modern Usage - Event sourcing, CQRS, and functional programming connections
Success Criteria:
By the end of this session, you should be able to implement a complete undo/redo system and understand how to apply Command pattern to decouple request senders from receivers.
3. Foundation Knowledge (15-20 minutes)
Core Concepts
What is the Command Pattern?
The Command pattern encapsulates a request as an object, thereby allowing you to parameterize clients with different requests, queue operations, and support undoable operations.
Real-World Analogy: Think of a restaurant order. The waiter writes down your order (command) on a ticket, which contains all the information needed to prepare your meal. The kitchen staff (receiver) doesn’t need to know who ordered it or when - they just follow the instructions on the ticket. The waiter (invoker) can hand the ticket to the kitchen, delay it, or even cancel it.
Why This Matters: The Command pattern decouples the object that invokes the operation from the object that performs it. This enables powerful features like undo/redo, macro recording, queuing, and logging.
Problem Without Command Pattern
// Tightly coupled approach
class DocumentEditor {
private Document document;
public void handleKeyPress(String key) {
if (key.equals("Ctrl+C")) {
document.copy(); // Direct coupling
} else if (key.equals("Ctrl+V")) {
document.paste(); // Hard to undo
} else if (key.equals("Ctrl+Z")) {
// How do we undo the last operation? We don't know what it was!
}
}
}
Key Terminology
- Command: Interface that declares method for executing operations
- Concrete Command: Implements Command interface and invokes methods on Receiver
- Receiver: Knows how to perform the work associated with a request
- Invoker: Asks the command to carry out the request
- Client: Creates ConcreteCommand objects and sets their receivers
4. Detailed Explanation and Examples (25-30 minutes)
Understanding Command Pattern Structure
The Command pattern involves four main components:
- Command Interface: Declares execute() method and optionally undo()
- Concrete Commands: Implement the Command interface and call receiver methods
- Receiver: Contains the business logic for performing operations
- Invoker: Stores commands and triggers their execution
Practical Applications
Command pattern is essential for:
- Undo/Redo Systems: Text editors, graphics applications, database transactions
- GUI Applications: Menu items, buttons, keyboard shortcuts
- Macro Recording: Automating sequences of operations
- Request Queuing: Job scheduling, background processing
- Transaction Processing: Database operations, financial systems
- Event Sourcing: Storing state changes as a sequence of events
Common Misconceptions
-
Misconception: Command pattern is just a fancy function pointer Reality: Commands can carry state, support undo, and be composed into complex operations
-
Misconception: Every method call needs to be a command Reality: Use commands for operations that need to be parameterized, queued, or undone
-
Misconception: Commands should contain business logic Reality: Commands should delegate to receivers; they’re coordinators, not implementers
5. Live Coding Demonstration (20-25 minutes)
Example 1: Text Editor with Undo/Redo (Java)
import java.util.*;
// Command interface
interface Command {
void execute();
void undo();
String getDescription();
}
// Receiver - the object that knows how to perform the work
class TextDocument {
private StringBuilder content;
private int cursorPosition;
public TextDocument() {
this.content = new StringBuilder();
this.cursorPosition = 0;
}
public void insertText(String text, int position) {
content.insert(position, text);
cursorPosition = position + text.length();
}
public String deleteText(int position, int length) {
String deleted = content.substring(position, Math.min(position + length, content.length()));
content.delete(position, Math.min(position + length, content.length()));
cursorPosition = position;
return deleted;
}
public void setCursorPosition(int position) {
this.cursorPosition = Math.max(0, Math.min(position, content.length()));
}
public String getContent() {
return content.toString();
}
public int getCursorPosition() {
return cursorPosition;
}
public int getLength() {
return content.length();
}
}
// Concrete Commands
class InsertTextCommand implements Command {
private TextDocument document;
private String text;
private int position;
public InsertTextCommand(TextDocument document, String text, int position) {
this.document = document;
this.text = text;
this.position = position;
}
@Override
public void execute() {
document.insertText(text, position);
}
@Override
public void undo() {
document.deleteText(position, text.length());
}
@Override
public String getDescription() {
return "Insert '" + text + "' at position " + position;
}
}
class DeleteTextCommand implements Command {
private TextDocument document;
private int position;
private int length;
private String deletedText; // Store for undo
public DeleteTextCommand(TextDocument document, int position, int length) {
this.document = document;
this.position = position;
this.length = length;
}
@Override
public void execute() {
deletedText = document.deleteText(position, length);
}
@Override
public void undo() {
if (deletedText != null) {
document.insertText(deletedText, position);
}
}
@Override
public String getDescription() {
return "Delete " + length + " characters at position " + position;
}
}
class ReplaceTextCommand implements Command {
private TextDocument document;
private int position;
private String oldText;
private String newText;
public ReplaceTextCommand(TextDocument document, int position, String oldText, String newText) {
this.document = document;
this.position = position;
this.oldText = oldText;
this.newText = newText;
}
@Override
public void execute() {
document.deleteText(position, oldText.length());
document.insertText(newText, position);
}
@Override
public void undo() {
document.deleteText(position, newText.length());
document.insertText(oldText, position);
}
@Override
public String getDescription() {
return "Replace '" + oldText + "' with '" + newText + "' at position " + position;
}
}
// Macro Command - composite command that executes multiple commands
class MacroCommand implements Command {
private List<Command> commands;
private String description;
public MacroCommand(String description) {
this.commands = new ArrayList<>();
this.description = description;
}
public void addCommand(Command command) {
commands.add(command);
}
@Override
public void execute() {
for (Command command : commands) {
command.execute();
}
}
@Override
public void undo() {
// Undo in reverse order
for (int i = commands.size() - 1; i >= 0; i--) {
commands.get(i).undo();
}
}
@Override
public String getDescription() {
return description + " (" + commands.size() + " operations)";
}
}
// Invoker - manages and executes commands
class TextEditor {
private TextDocument document;
private Stack<Command> history;
private Stack<Command> redoStack;
private int maxHistorySize;
public TextEditor() {
this.document = new TextDocument();
this.history = new Stack<>();
this.redoStack = new Stack<>();
this.maxHistorySize = 100;
}
public void executeCommand(Command command) {
command.execute();
history.push(command);
redoStack.clear(); // Clear redo stack when new command is executed
// Limit history size
if (history.size() > maxHistorySize) {
history.remove(0);
}
System.out.println("Executed: " + command.getDescription());
System.out.println("Document: \"" + document.getContent() + "\"");
}
public boolean undo() {
if (history.isEmpty()) {
System.out.println("Nothing to undo");
return false;
}
Command lastCommand = history.pop();
lastCommand.undo();
redoStack.push(lastCommand);
System.out.println("Undid: " + lastCommand.getDescription());
System.out.println("Document: \"" + document.getContent() + "\"");
return true;
}
public boolean redo() {
if (redoStack.isEmpty()) {
System.out.println("Nothing to redo");
return false;
}
Command commandToRedo = redoStack.pop();
commandToRedo.execute();
history.push(commandToRedo);
System.out.println("Redid: " + commandToRedo.getDescription());
System.out.println("Document: \"" + document.getContent() + "\"");
return true;
}
public void showHistory() {
System.out.println("\n=== Command History ===");
for (int i = 0; i < history.size(); i++) {
System.out.println((i + 1) + ". " + history.get(i).getDescription());
}
System.out.println("=======================\n");
}
public TextDocument getDocument() {
return document;
}
}
// Usage demonstration
class TextEditorDemo {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
TextDocument doc = editor.getDocument();
// Simple operations
editor.executeCommand(new InsertTextCommand(doc, "Hello", 0));
editor.executeCommand(new InsertTextCommand(doc, " World", 5));
editor.executeCommand(new InsertTextCommand(doc, "!", 11));
// Create a macro for formatting
MacroCommand formatMacro = new MacroCommand("Format text");
formatMacro.addCommand(new ReplaceTextCommand(doc, 0, "Hello", "HELLO"));
formatMacro.addCommand(new InsertTextCommand(doc, " - ", 11));
formatMacro.addCommand(new InsertTextCommand(doc, "FORMATTED", 14));
editor.executeCommand(formatMacro);
editor.showHistory();
// Test undo/redo
editor.undo(); // Undo macro
editor.undo(); // Undo "!"
editor.redo(); // Redo "!"
editor.showHistory();
}
}
Explanation:
- Commands encapsulate operations and their undo logic
- The editor maintains command history for undo/redo
- Macro commands demonstrate composition of multiple operations
- Each command stores necessary information to reverse its operation
Example 2: Smart Home Control System (Python)
from abc import ABC, abstractmethod
from datetime import datetime
from typing import List, Dict, Any
import time
# Command interface
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
pass
@abstractmethod
def undo(self) -> bool:
pass
@abstractmethod
def get_description(self) -> str:
pass
# Receivers - Smart home devices
class SmartLight:
def __init__(self, room: str):
self.room = room
self.is_on = False
self.brightness = 0
self.color = "white"
def turn_on(self):
self.is_on = True
self.brightness = 100
print(f"š” {self.room} light turned ON")
def turn_off(self):
self.is_on = False
self.brightness = 0
print(f"š” {self.room} light turned OFF")
def set_brightness(self, brightness: int):
if self.is_on:
self.brightness = max(0, min(100, brightness))
print(f"š” {self.room} light brightness set to {self.brightness}%")
else:
print(f"š” {self.room} light is off, cannot set brightness")
def set_color(self, color: str):
if self.is_on:
self.color = color
print(f"š” {self.room} light color set to {color}")
else:
print(f"š” {self.room} light is off, cannot set color")
def get_status(self) -> Dict[str, Any]:
return {
"room": self.room,
"is_on": self.is_on,
"brightness": self.brightness,
"color": self.color
}
class SmartThermostat:
def __init__(self):
self.temperature = 70
self.mode = "auto"
def set_temperature(self, temp: int):
self.temperature = temp
print(f"š”ļø Thermostat set to {temp}°F")
def set_mode(self, mode: str):
self.mode = mode
print(f"š”ļø Thermostat mode set to {mode}")
def get_status(self) -> Dict[str, Any]:
return {
"temperature": self.temperature,
"mode": self.mode
}
class SmartSpeaker:
def __init__(self, location: str):
self.location = location
self.volume = 50
self.is_playing = False
self.current_song = None
def play_music(self, song: str):
self.is_playing = True
self.current_song = song
print(f"šµ {self.location} speaker playing: {song}")
def stop_music(self):
self.is_playing = False
print(f"šµ {self.location} speaker stopped")
return self.current_song
def set_volume(self, volume: int):
self.volume = max(0, min(100, volume))
print(f"šµ {self.location} speaker volume set to {self.volume}")
# Concrete Commands
class LightOnCommand(Command):
def __init__(self, light: SmartLight):
self.light = light
self.previous_state = None
def execute(self) -> bool:
self.previous_state = self.light.get_status()
self.light.turn_on()
return True
def undo(self) -> bool:
if self.previous_state and not self.previous_state['is_on']:
self.light.turn_off()
return True
def get_description(self) -> str:
return f"Turn on {self.light.room} light"
class LightOffCommand(Command):
def __init__(self, light: SmartLight):
self.light = light
self.previous_state = None
def execute(self) -> bool:
self.previous_state = self.light.get_status()
self.light.turn_off()
return True
def undo(self) -> bool:
if self.previous_state and self.previous_state['is_on']:
self.light.turn_on()
self.light.set_brightness(self.previous_state['brightness'])
self.light.set_color(self.previous_state['color'])
return True
def get_description(self) -> str:
return f"Turn off {self.light.room} light"
class SetBrightnessCommand(Command):
def __init__(self, light: SmartLight, brightness: int):
self.light = light
self.brightness = brightness
self.previous_brightness = None
def execute(self) -> bool:
self.previous_brightness = self.light.brightness
self.light.set_brightness(self.brightness)
return True
def undo(self) -> bool:
if self.previous_brightness is not None:
self.light.set_brightness(self.previous_brightness)
return True
def get_description(self) -> str:
return f"Set {self.light.room} light brightness to {self.brightness}%"
class SetTemperatureCommand(Command):
def __init__(self, thermostat: SmartThermostat, temperature: int):
self.thermostat = thermostat
self.temperature = temperature
self.previous_temperature = None
def execute(self) -> bool:
self.previous_temperature = self.thermostat.temperature
self.thermostat.set_temperature(self.temperature)
return True
def undo(self) -> bool:
if self.previous_temperature is not None:
self.thermostat.set_temperature(self.previous_temperature)
return True
def get_description(self) -> str:
return f"Set temperature to {self.temperature}°F"
class PlayMusicCommand(Command):
def __init__(self, speaker: SmartSpeaker, song: str):
self.speaker = speaker
self.song = song
self.was_playing = False
self.previous_song = None
def execute(self) -> bool:
self.was_playing = self.speaker.is_playing
self.previous_song = self.speaker.current_song
self.speaker.play_music(self.song)
return True
def undo(self) -> bool:
if self.was_playing and self.previous_song:
self.speaker.play_music(self.previous_song)
else:
self.speaker.stop_music()
return True
def get_description(self) -> str:
return f"Play '{self.song}' on {self.speaker.location} speaker"
# Scene Command - composite command for multiple device control
class SceneCommand(Command):
def __init__(self, name: str):
self.name = name
self.commands: List[Command] = []
def add_command(self, command: Command):
self.commands.append(command)
def execute(self) -> bool:
print(f"š¬ Activating scene: {self.name}")
success = True
for command in self.commands:
if not command.execute():
success = False
return success
def undo(self) -> bool:
print(f"š¬ Deactivating scene: {self.name}")
success = True
# Undo in reverse order
for command in reversed(self.commands):
if not command.undo():
success = False
return success
def get_description(self) -> str:
return f"Scene: {self.name} ({len(self.commands)} commands)"
# Scheduled Command - command with execution time
class ScheduledCommand(Command):
def __init__(self, command: Command, scheduled_time: datetime):
self.command = command
self.scheduled_time = scheduled_time
self.executed = False
def execute(self) -> bool:
print(f"ā° Executing scheduled command at {self.scheduled_time}")
self.executed = True
return self.command.execute()
def undo(self) -> bool:
if self.executed:
return self.command.undo()
return True
def get_description(self) -> str:
return f"Scheduled for {self.scheduled_time}: {self.command.get_description()}"
# Invoker - Smart Home Controller
class SmartHomeController:
def __init__(self):
self.command_history: List[Command] = []
self.undo_stack: List[Command] = []
self.scheduled_commands: List[ScheduledCommand] = []
def execute_command(self, command: Command) -> bool:
try:
success = command.execute()
if success:
self.command_history.append(command)
self.undo_stack.clear() # Clear redo stack
# Limit history size
if len(self.command_history) > 50:
self.command_history.pop(0)
return success
except Exception as e:
print(f"ā Error executing command: {e}")
return False
def undo_last_command(self) -> bool:
if not self.command_history:
print("š« No commands to undo")
return False
last_command = self.command_history.pop()
try:
success = last_command.undo()
if success:
self.undo_stack.append(last_command)
print(f"ā©ļø Undid: {last_command.get_description()}")
return success
except Exception as e:
print(f"ā Error undoing command: {e}")
return False
def redo_last_command(self) -> bool:
if not self.undo_stack:
print("š« No commands to redo")
return False
command_to_redo = self.undo_stack.pop()
return self.execute_command(command_to_redo)
def schedule_command(self, command: Command, scheduled_time: datetime):
scheduled_cmd = ScheduledCommand(command, scheduled_time)
self.scheduled_commands.append(scheduled_cmd)
print(f"š
Scheduled: {command.get_description()} for {scheduled_time}")
def execute_scheduled_commands(self):
"""Execute any scheduled commands that are due"""
current_time = datetime.now()
due_commands = [cmd for cmd in self.scheduled_commands
if cmd.scheduled_time <= current_time and not cmd.executed]
for cmd in due_commands:
self.execute_command(cmd)
def show_history(self):
print("\nš Command History:")
for i, cmd in enumerate(self.command_history, 1):
print(f" {i}. {cmd.get_description()}")
print()
def show_scheduled_commands(self):
print("\nš
Scheduled Commands:")
for cmd in self.scheduled_commands:
status = "ā
Executed" if cmd.executed else "ā³ Pending"
print(f" {cmd.get_description()} - {status}")
print()
# Demo usage
def smart_home_demo():
# Create devices
living_room_light = SmartLight("Living Room")
bedroom_light = SmartLight("Bedroom")
thermostat = SmartThermostat()
speaker = SmartSpeaker("Living Room")
# Create controller
controller = SmartHomeController()
print("š Smart Home Control System Demo\n")
# Individual commands
controller.execute_command(LightOnCommand(living_room_light))
controller.execute_command(SetBrightnessCommand(living_room_light, 75))
controller.execute_command(SetTemperatureCommand(thermostat, 72))
# Create a "Movie Night" scene
movie_scene = SceneCommand("Movie Night")
movie_scene.add_command(LightOffCommand(living_room_light))
movie_scene.add_command(LightOnCommand(bedroom_light))
movie_scene.add_command(SetBrightnessCommand(bedroom_light, 20))
movie_scene.add_command(SetTemperatureCommand(thermostat, 68))
movie_scene.add_command(PlayMusicCommand(speaker, "Relaxing Instrumental"))
print("\n" + "="*50)
controller.execute_command(movie_scene)
controller.show_history()
# Test undo/redo
print("Testing undo/redo:")
controller.undo_last_command() # Undo movie scene
controller.redo_last_command() # Redo movie scene
controller.show_history()
if __name__ == "__main__":
smart_home_demo()
Example 3: Database Transaction System (JavaScript)
// Command interface
class Command {
execute() {
throw new Error("execute() must be implemented");
}
undo() {
throw new Error("undo() must be implemented");
}
getDescription() {
throw new Error("getDescription() must be implemented");
}
}
// Mock database for demonstration
class Database {
constructor() {
this.tables = new Map();
this.nextId = 1;
}
createTable(tableName) {
if (this.tables.has(tableName)) {
throw new Error(`Table ${tableName} already exists`);
}
this.tables.set(tableName, new Map());
console.log(`š Created table: ${tableName}`);
}
dropTable(tableName) {
if (!this.tables.has(tableName)) {
throw new Error(`Table ${tableName} does not exist`);
}
const backup = new Map(this.tables.get(tableName));
this.tables.delete(tableName);
console.log(`šļø Dropped table: ${tableName}`);
return backup;
}
insert(tableName, record) {
if (!this.tables.has(tableName)) {
throw new Error(`Table ${tableName} does not exist`);
}
const id = this.nextId++;
const recordWithId = { id, ...record };
this.tables.get(tableName).set(id, recordWithId);
console.log(`ā Inserted record ${id} into ${tableName}:`, recordWithId);
return id;
}
update(tableName, id, updates) {
if (!this.tables.has(tableName)) {
throw new Error(`Table ${tableName} does not exist`);
}
const table = this.tables.get(tableName);
if (!table.has(id)) {
throw new Error(`Record ${id} not found in ${tableName}`);
}
const originalRecord = { ...table.get(id) };
const updatedRecord = { ...originalRecord, ...updates };
table.set(id, updatedRecord);
console.log(`āļø Updated record ${id} in ${tableName}:`, updatedRecord);
return originalRecord;
}
delete(tableName, id) {
if (!this.tables.has(tableName)) {
throw new Error(`Table ${tableName} does not exist`);
}
const table = this.tables.get(tableName);
if (!table.has(id)) {
throw new Error(`Record ${id} not found in ${tableName}`);
}
const deletedRecord = table.get(id);
table.delete(id);
console.log(`ā Deleted record ${id} from ${tableName}:`, deletedRecord);
return deletedRecord;
}
select(tableName, id = null) {
if (!this.tables.has(tableName)) {
throw new Error(`Table ${tableName} does not exist`);
}
const table = this.tables.get(tableName);
if (id !== null) {
return table.get(id) || null;
}
return Array.from(table.values());
}
showTables() {
console.log("\nš Database Tables:");
for (const [tableName, table] of this.tables) {
console.log(` ${tableName}: ${table.size} records`);
}
console.log();
}
}
// Concrete Commands
class CreateTableCommand extends Command {
constructor(database, tableName) {
super();
this.database = database;
this.tableName = tableName;
this.executed = false;
}
execute() {
this.database.createTable(this.tableName);
this.executed = true;
}
undo() {
if (this.executed) {
this.database.dropTable(this.tableName);
}
}
getDescription() {
return `CREATE TABLE ${this.tableName}`;
}
}
class DropTableCommand extends Command {
constructor(database, tableName) {
super();
this.database = database;
this.tableName = tableName;
this.backupData = null;
}
execute() {
this.backupData = this.database.dropTable(this.tableName);
}
undo() {
if (this.backupData !== null) {
this.database.createTable(this.tableName);
const table = this.database.tables.get(this.tableName);
for (const [id, record] of this.backupData) {
table.set(id, record);
}
}
}
getDescription() {
return `DROP TABLE ${this.tableName}`;
}
}
class InsertCommand extends Command {
constructor(database, tableName, record) {
super();
this.database = database;
this.tableName = tableName;
this.record = record;
this.insertedId = null;
}
execute() {
this.insertedId = this.database.insert(this.tableName, this.record);
}
undo() {
if (this.insertedId !== null) {
this.database.delete(this.tableName, this.insertedId);
}
}
getDescription() {
return `INSERT INTO ${this.tableName} (${Object.keys(this.record).join(', ')})`;
}
}
class UpdateCommand extends Command {
constructor(database, tableName, id, updates) {
super();
this.database = database;
this.tableName = tableName;
this.id = id;
this.updates = updates;
this.originalRecord = null;
}
execute() {
this.originalRecord = this.database.update(this.tableName, this.id, this.updates);
}
undo() {
if (this.originalRecord !== null) {
this.database.update(this.tableName, this.id, this.originalRecord);
}
}
getDescription() {
return `UPDATE ${this.tableName} SET ${Object.keys(this.updates).join(', ')} WHERE id=${this.id}`;
}
}
class DeleteCommand extends Command {
constructor(database, tableName, id) {
super();
this.database = database;
this.tableName = tableName;
this.id = id;
this.deletedRecord = null;
}
execute() {
this.deletedRecord = this.database.delete(this.tableName, this.id);
}
undo() {
if (this.deletedRecord !== null) {
// Re-insert with the same ID
const table = this.database.tables.get(this.tableName);
table.set(this.id, this.deletedRecord);
console.log(`š Restored record ${this.id} to ${this.tableName}:`, this.deletedRecord);
}
}
getDescription() {
return `DELETE FROM ${this.tableName} WHERE id=${this.id}`;
}
}
// Transaction Command - composite command with rollback
class TransactionCommand extends Command {
constructor(description = "Transaction") {
super();
this.description = description;
this.commands = [];
this.executed = false;
this.partiallyExecuted = 0;
}
addCommand(command) {
this.commands.push(command);
}
execute() {
console.log(`š Starting transaction: ${this.description}`);
try {
for (let i = 0; i < this.commands.length; i++) {
this.commands[i].execute();
this.partiallyExecuted = i + 1;
}
this.executed = true;
console.log(`ā
Transaction completed: ${this.description}`);
} catch (error) {
console.log(`ā Transaction failed: ${error.message}`);
this.rollback();
throw error;
}
}
undo() {
this.rollback();
}
rollback() {
console.log(`ā©ļø Rolling back transaction: ${this.description}`);
// Undo in reverse order, only up to where we got
for (let i = this.partiallyExecuted - 1; i >= 0; i--) {
try {
this.commands[i].undo();
} catch (error) {
console.log(`ā ļø Error during rollback: ${error.message}`);
}
}
this.executed = false;
this.partiallyExecuted = 0;
console.log(`š Rollback completed for: ${this.description}`);
}
getDescription() {
return `${this.description} (${this.commands.length} operations)`;
}
}
// Database Transaction Manager (Invoker)
class TransactionManager {
constructor(database) {
this.database = database;
this.history = [];
this.undoStack = [];
this.maxHistorySize = 50;
}
executeCommand(command) {
try {
command.execute();
this.history.push(command);
this.undoStack = []; // Clear redo stack
// Limit history size
if (this.history.length > this.maxHistorySize) {
this.history.shift();
}
console.log(`š Executed: ${command.getDescription()}`);
return true;
} catch (error) {
console.log(`ā Failed to execute: ${command.getDescription()} - ${error.message}`);
return false;
}
}
undo() {
if (this.history.length === 0) {
console.log("š« Nothing to undo");
return false;
}
const lastCommand = this.history.pop();
try {
lastCommand.undo();
this.undoStack.push(lastCommand);
console.log(`ā©ļø Undid: ${lastCommand.getDescription()}`);
return true;
} catch (error) {
console.log(`ā Failed to undo: ${lastCommand.getDescription()} - ${error.message}`);
return false;
}
}
redo() {
if (this.undoStack.length === 0) {
console.log("š« Nothing to redo");
return false;
}
const commandToRedo = this.undoStack.pop();
return this.executeCommand(commandToRedo);
}
showHistory() {
console.log("\nš Transaction History:");
this.history.forEach((cmd, index) => {
console.log(` ${index + 1}. ${cmd.getDescription()}`);
});
if (this.history.length === 0) {
console.log(" (No transactions)");
}
console.log();
}
beginTransaction(description = "Batch Transaction") {
return new TransactionCommand(description);
}
}
// Demo usage
function databaseDemo() {
console.log("š¾ Database Transaction System Demo\n");
const db = new Database();
const manager = new TransactionManager(db);
// Individual commands
manager.executeCommand(new CreateTableCommand(db, "users"));
manager.executeCommand(new CreateTableCommand(db, "orders"));
manager.executeCommand(new InsertCommand(db, "users", {
name: "John Doe",
email: "john@example.com",
age: 30
}));
manager.executeCommand(new InsertCommand(db, "users", {
name: "Jane Smith",
email: "jane@example.com",
age: 25
}));
db.showTables();
// Create a complex transaction
const setupTransaction = manager.beginTransaction("User Setup Transaction");
setupTransaction.addCommand(new InsertCommand(db, "users", {
name: "Bob Wilson",
email: "bob@example.com",
age: 35
}));
setupTransaction.addCommand(new InsertCommand(db, "orders", {
userId: 3,
product: "Laptop",
amount: 1299.99
}));
setupTransaction.addCommand(new InsertCommand(db, "orders", {
userId: 3,
product: "Mouse",
amount: 29.99
}));
console.log("\n" + "=".repeat(50));
manager.executeCommand(setupTransaction);
db.showTables();
manager.showHistory();
// Test undo/redo
console.log("Testing undo/redo:");
manager.undo(); // Undo the transaction
db.showTables();
manager.redo(); // Redo the transaction
db.showTables();
// Test failed transaction with rollback
console.log("\n" + "=".repeat(50));
console.log("Testing transaction rollback on failure:");
const failingTransaction = manager.beginTransaction("Failing Transaction");
failingTransaction.addCommand(new InsertCommand(db, "users", {
name: "Test User",
email: "test@example.com",
age: 20
}));
failingTransaction.addCommand(new CreateTableCommand(db, "invalid_table"));
failingTransaction.addCommand(new CreateTableCommand(db, "users")); // This will fail - table already exists
try {
manager.executeCommand(failingTransaction);
} catch (error) {
console.log("Transaction was properly rolled back");
}
db.showTables();
manager.showHistory();
}
// Run the demo
databaseDemo();
6. Hands-On Practice (30-40 minutes)
Exercise 1: Calculator with Undo (15 minutes)
Objective: Create a calculator that supports undo operations for arithmetic
Instructions:
- Create a
Calculatorclass with methods:add(),subtract(),multiply(),divide() - Design commands for each operation that can be undone
- Implement a
CalculatorInvokerthat maintains command history - Support undo/redo functionality
Starter Code:
class Calculator {
private double result = 0;
public void add(double value) { result += value; }
public void subtract(double value) { result -= value; }
public void multiply(double value) { result *= value; }
public void divide(double value) {
if (value != 0) result /= value;
else throw new IllegalArgumentException("Division by zero");
}
public double getResult() { return result; }
public void setResult(double value) { result = value; }
}
// Your task: Create Command interface and concrete commands
Solution:
interface CalculatorCommand {
void execute();
void undo();
String getDescription();
}
class AddCommand implements CalculatorCommand {
private Calculator calculator;
private double value;
public AddCommand(Calculator calculator, double value) {
this.calculator = calculator;
this.value = value;
}
@Override
public void execute() {
calculator.add(value);
}
@Override
public void undo() {
calculator.subtract(value);
}
@Override
public String getDescription() {
return "Add " + value;
}
}
class SubtractCommand implements CalculatorCommand {
private Calculator calculator;
private double value;
public SubtractCommand(Calculator calculator, double value) {
this.calculator = calculator;
this.value = value;
}
@Override
public void execute() {
calculator.subtract(value);
}
@Override
public void undo() {
calculator.add(value);
}
@Override
public String getDescription() {
return "Subtract " + value;
}
}
class MultiplyCommand implements CalculatorCommand {
private Calculator calculator;
private double value;
private double previousResult;
public MultiplyCommand(Calculator calculator, double value) {
this.calculator = calculator;
this.value = value;
}
@Override
public void execute() {
previousResult = calculator.getResult();
calculator.multiply(value);
}
@Override
public void undo() {
calculator.setResult(previousResult);
}
@Override
public String getDescription() {
return "Multiply by " + value;
}
}
class CalculatorInvoker {
private Calculator calculator;
private Stack<CalculatorCommand> history;
public CalculatorInvoker(Calculator calculator) {
this.calculator = calculator;
this.history = new Stack<>();
}
public void executeCommand(CalculatorCommand command) {
command.execute();
history.push(command);
System.out.println("Executed: " + command.getDescription() + " = " + calculator.getResult());
}
public void undo() {
if (!history.isEmpty()) {
CalculatorCommand lastCommand = history.pop();
lastCommand.undo();
System.out.println("Undid: " + lastCommand.getDescription() + " = " + calculator.getResult());
}
}
}
Exercise 2: Drawing Application with Command Pattern (25 minutes)
Objective: Create a drawing application where drawing operations can be undone
Requirements:
- Support drawing different shapes (line, rectangle, circle)
- Each shape operation should be undoable
- Implement composite commands for complex drawings
- Add support for saving/loading command sequences
Challenge: Create a “replay” feature that can execute a saved sequence of drawing commands
Extension Ideas:
- Add copy/paste functionality using commands
- Implement shape transformation commands (move, resize, rotate)
- Create macro commands for drawing complex patterns
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
-
Problem: Commands contain too much business logic Cause: Putting domain logic in commands instead of receivers Solution: Commands should delegate to receivers; keep them thin coordinators
-
Problem: Undo operations don’t properly restore state Cause: Not capturing sufficient state before execution Solution: Store all necessary state in command before executing, test undo thoroughly
-
Problem: Memory leaks from unlimited command history Cause: Never clearing command history or storing too much state Solution: Implement history size limits and clean up unused commands
-
Problem: Commands become tightly coupled to specific receivers Cause: Commands directly accessing receiver internals Solution: Use well-defined receiver interfaces and avoid exposing implementation details
Debugging Tips
- Test undo immediately after implementing each command
- Verify state consistency before and after undo operations
- Use immutable objects where possible to avoid state corruption
- Log command execution for debugging complex scenarios
- Test edge cases like undoing when history is empty
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- Keep commands stateless where possible, store only necessary state for undo
- Implement proper error handling in both execute and undo methods
- Use descriptive command names that clearly indicate their purpose
- Limit command history size to prevent memory issues
Design Principles
- Single Responsibility: Commands should have one clear purpose
- Command Interface Consistency: All commands should implement the same interface
- Receiver Independence: Commands shouldn’t make assumptions about receiver implementation
Performance Considerations
- Lazy State Capture: Only store state needed for undo, not entire object copies
- Command Pooling: Reuse command objects for frequently executed operations
- Batch Operations: Use composite commands for multiple related operations
Modern Alternatives and Extensions
- Event Sourcing: Store commands as events in an event store
- CQRS (Command Query Responsibility Segregation): Separate command and query models
- Functional Programming: Use immutable data structures and pure functions
- Redux Pattern: Command-like actions with reducers for state management
Next Steps in Your Learning Journey
- Study Event Sourcing patterns for persistent command storage
- Learn CQRS for separating command and query responsibilities
- Explore Functional Programming approaches to command patterns
- Practice with GUI frameworks that heavily use command patterns
9. Summary and Review (5 minutes)
Key Takeaways
- Command pattern decouples request senders from receivers through object encapsulation
- Undo/redo functionality becomes straightforward with proper state management
- Composite commands enable complex operations with consistent undo behavior
- Real-world applications include GUI frameworks, transaction systems, and automation tools
Quick Review Questions
- What are the four main components of the Command pattern?
- How does Command pattern enable undo functionality?
- When would you use a macro command vs individual commands?
For Instructors
Teaching Tips
- Start with simple examples like a remote control before moving to complex systems
- Emphasize undo implementation as it’s often the most challenging part
- Show real-world examples from GUI frameworks students are familiar with
- Demonstrate debugging of command sequences and undo operations
- Use visual aids to show command flow and state changes
Common Student Questions
- “When should I use Command vs Strategy pattern?” - Command for parameterizing operations and undo, Strategy for algorithms
- “How do I handle commands that can’t be undone?” - Mark them as non-undoable and document clearly
- “Is Command pattern the same as the Action pattern?” - Similar concepts, Action is more specific to event handling
Assessment Ideas
- Undo/redo implementation projects testing state management
- Transaction system design challenges
- GUI application with command-based operations
- Code review exercises identifying command pattern opportunities
Adaptation Notes
- For beginners: Focus on simple commands before introducing composites and undo
- For advanced students: Explore event sourcing and CQRS patterns
- Language-specific notes: Discuss closures and functional approaches in appropriate languages
- Time management: The hands-on exercises can be simplified or extended based on available time