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:

  1. Request Encapsulation - Turning method calls into objects
  2. Command Pattern Structure - Command, Receiver, Invoker, and Client relationships
  3. Undo/Redo Implementation - Reversible operations and command history
  4. Advanced Patterns - Macro commands, queuing, logging, and transactions
  5. Real-world Applications - GUI actions, database transactions, batch processing
  6. 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:

  1. Command Interface: Declares execute() method and optionally undo()
  2. Concrete Commands: Implement the Command interface and call receiver methods
  3. Receiver: Contains the business logic for performing operations
  4. 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

  1. Misconception: Command pattern is just a fancy function pointer Reality: Commands can carry state, support undo, and be composed into complex operations

  2. Misconception: Every method call needs to be a command Reality: Use commands for operations that need to be parameterized, queued, or undone

  3. 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:

  1. Create a Calculator class with methods: add(), subtract(), multiply(), divide()
  2. Design commands for each operation that can be undone
  3. Implement a CalculatorInvoker that maintains command history
  4. 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

  1. 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

  2. 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

  3. 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

  4. 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

  1. Command pattern decouples request senders from receivers through object encapsulation
  2. Undo/redo functionality becomes straightforward with proper state management
  3. Composite commands enable complex operations with consistent undo behavior
  4. Real-world applications include GUI frameworks, transaction systems, and automation tools

Quick Review Questions

  1. What are the four main components of the Command pattern?
  2. How does Command pattern enable undo functionality?
  3. 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