+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 296 of 355

๐Ÿ“˜ Command Pattern: Encapsulated Requests

Master command pattern: encapsulated requests in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write type-safe code โœจ

๐ŸŽฏ Introduction

Have you ever wished you could treat actions like objects? ๐Ÿค” Imagine being able to store, queue, undo, and redo operations just like you handle regular data! Thatโ€™s exactly what the Command Pattern brings to the table! ๐ŸŽ‰

The Command Pattern is like having a remote control ๐ŸŽฎ where each button is a command that can be executed, undone, or even saved for later. Itโ€™s one of the most powerful behavioral patterns in TypeScript, and today weโ€™re going to make it your new superpower! ๐Ÿ’ช

๐Ÿ“š Understanding Command Pattern

Think of the Command Pattern like ordering at a restaurant ๐Ÿฝ๏ธ. When you tell the waiter what you want, they write it down on a piece of paper (the command). This order can be:

  • Passed to the kitchen (executed) ๐Ÿ‘จโ€๐Ÿณ
  • Modified or cancelled (undone) โŒ
  • Tracked for billing (logged) ๐Ÿ“
  • Queued during busy times (delayed execution) โฐ

In programming terms, the Command Pattern encapsulates a request as an object, allowing you to:

  • Parameterize objects with different requests ๐Ÿ“ฆ
  • Queue or log requests ๐Ÿ“‹
  • Support undo operations โ†ฉ๏ธ
  • Build macro commands from smaller ones ๐Ÿ—๏ธ

๐Ÿ”ง Basic Syntax and Usage

Letโ€™s start with a simple command pattern implementation:

// ๐ŸŽฏ The Command interface
interface Command {
  execute(): void;
  undo(): void;
}

// ๐Ÿ’ก Receiver - the object that performs the actual work
class Light {
  private isOn = false;

  turnOn(): void {
    this.isOn = true;
    console.log("Light is ON! ๐Ÿ’ก");
  }

  turnOff(): void {
    this.isOn = false;
    console.log("Light is OFF! ๐ŸŒ‘");
  }

  getStatus(): string {
    return this.isOn ? "ON ๐Ÿ’ก" : "OFF ๐ŸŒ‘";
  }
}

// ๐ŸŽฎ Concrete Command
class TurnOnLightCommand implements Command {
  constructor(private light: Light) {}

  execute(): void {
    this.light.turnOn();
  }

  undo(): void {
    this.light.turnOff();
  }
}

// ๐ŸŽฎ Another Concrete Command
class TurnOffLightCommand implements Command {
  constructor(private light: Light) {}

  execute(): void {
    this.light.turnOff();
  }

  undo(): void {
    this.light.turnOn();
  }
}

// ๐ŸŽ›๏ธ Invoker - the remote control
class RemoteControl {
  private currentCommand: Command | null = null;

  setCommand(command: Command): void {
    this.currentCommand = command;
  }

  pressButton(): void {
    if (this.currentCommand) {
      this.currentCommand.execute();
    }
  }

  pressUndo(): void {
    if (this.currentCommand) {
      this.currentCommand.undo();
    }
  }
}

// ๐Ÿš€ Let's use it!
const livingRoomLight = new Light();
const remote = new RemoteControl();

const turnOn = new TurnOnLightCommand(livingRoomLight);
const turnOff = new TurnOffLightCommand(livingRoomLight);

remote.setCommand(turnOn);
remote.pressButton(); // Light is ON! ๐Ÿ’ก
remote.pressUndo();   // Light is OFF! ๐ŸŒ‘

๐Ÿ’ก Practical Examples

Example 1: Text Editor with Undo/Redo ๐Ÿ“

Letโ€™s build a simple text editor that supports undo and redo operations:

// ๐Ÿ“„ The document we're editing
class TextDocument {
  private content = "";

  write(text: string): void {
    this.content += text;
  }

  delete(length: number): void {
    this.content = this.content.substring(0, this.content.length - length);
  }

  getContent(): string {
    return this.content;
  }

  setContent(content: string): void {
    this.content = content;
  }
}

// ๐ŸŽฏ Abstract command with state saving
abstract class TextCommand implements Command {
  protected previousState = "";

  constructor(protected document: TextDocument) {}

  abstract execute(): void;
  abstract undo(): void;
}

// โœ๏ธ Write command
class WriteCommand extends TextCommand {
  constructor(document: TextDocument, private text: string) {
    super(document);
  }

  execute(): void {
    this.previousState = this.document.getContent();
    this.document.write(this.text);
    console.log(`Wrote: "${this.text}" ๐Ÿ“`);
  }

  undo(): void {
    this.document.setContent(this.previousState);
    console.log(`Undid write of: "${this.text}" โ†ฉ๏ธ`);
  }
}

// โœ‚๏ธ Delete command
class DeleteCommand extends TextCommand {
  private deletedText = "";

  constructor(document: TextDocument, private length: number) {
    super(document);
  }

  execute(): void {
    const content = this.document.getContent();
    this.deletedText = content.substring(content.length - this.length);
    this.document.delete(this.length);
    console.log(`Deleted: "${this.deletedText}" ๐Ÿ—‘๏ธ`);
  }

  undo(): void {
    this.document.write(this.deletedText);
    console.log(`Restored: "${this.deletedText}" ๐Ÿ”„`);
  }
}

// ๐Ÿ“š Command History Manager
class CommandHistory {
  private history: Command[] = [];
  private currentIndex = -1;

  execute(command: Command): void {
    // Remove any commands after current index
    this.history = this.history.slice(0, this.currentIndex + 1);
    
    // Add new command
    command.execute();
    this.history.push(command);
    this.currentIndex++;
  }

  undo(): void {
    if (this.currentIndex >= 0) {
      const command = this.history[this.currentIndex];
      command.undo();
      this.currentIndex--;
      console.log("Undo successful! โ†ฉ๏ธ");
    } else {
      console.log("Nothing to undo! ๐Ÿคท");
    }
  }

  redo(): void {
    if (this.currentIndex < this.history.length - 1) {
      this.currentIndex++;
      const command = this.history[this.currentIndex];
      command.execute();
      console.log("Redo successful! โ†ช๏ธ");
    } else {
      console.log("Nothing to redo! ๐Ÿคท");
    }
  }
}

// ๐ŸŽฎ Let's test our text editor!
const doc = new TextDocument();
const history = new CommandHistory();

history.execute(new WriteCommand(doc, "Hello "));    // Wrote: "Hello " ๐Ÿ“
history.execute(new WriteCommand(doc, "World!"));    // Wrote: "World!" ๐Ÿ“
console.log(`Document: "${doc.getContent()}"`);      // Document: "Hello World!"

history.undo();                                      // Undid write of: "World!" โ†ฉ๏ธ
console.log(`Document: "${doc.getContent()}"`);      // Document: "Hello "

history.redo();                                      // Wrote: "World!" ๐Ÿ“
console.log(`Document: "${doc.getContent()}"`);      // Document: "Hello World!"

history.execute(new DeleteCommand(doc, 6));          // Deleted: "World!" ๐Ÿ—‘๏ธ
console.log(`Document: "${doc.getContent()}"`);      // Document: "Hello "

Example 2: Smart Home Automation ๐Ÿ 

Letโ€™s create a smart home system with macro commands:

// ๐Ÿ  Various home devices
class Television {
  turnOn(): void { console.log("TV is ON ๐Ÿ“บ"); }
  turnOff(): void { console.log("TV is OFF ๐Ÿ“บ"); }
  setChannel(channel: number): void { console.log(`Channel set to ${channel} ๐Ÿ“บ`); }
}

class AirConditioner {
  turnOn(): void { console.log("AC is ON โ„๏ธ"); }
  turnOff(): void { console.log("AC is OFF โ„๏ธ"); }
  setTemperature(temp: number): void { console.log(`Temperature set to ${temp}ยฐC ๐ŸŒก๏ธ`); }
}

class SecuritySystem {
  arm(): void { console.log("Security ARMED ๐Ÿ”’"); }
  disarm(): void { console.log("Security DISARMED ๐Ÿ”“"); }
}

// ๐ŸŽฏ Device commands
class TVOnCommand implements Command {
  constructor(private tv: Television, private channel: number = 1) {}
  
  execute(): void {
    this.tv.turnOn();
    this.tv.setChannel(this.channel);
  }
  
  undo(): void {
    this.tv.turnOff();
  }
}

class ACOnCommand implements Command {
  constructor(private ac: AirConditioner, private temperature: number = 22) {}
  
  execute(): void {
    this.ac.turnOn();
    this.ac.setTemperature(this.temperature);
  }
  
  undo(): void {
    this.ac.turnOff();
  }
}

class SecurityArmCommand implements Command {
  constructor(private security: SecuritySystem) {}
  
  execute(): void {
    this.security.arm();
  }
  
  undo(): void {
    this.security.disarm();
  }
}

// ๐ŸŽญ Macro Command - executes multiple commands
class MacroCommand implements Command {
  constructor(private commands: Command[]) {}
  
  execute(): void {
    console.log("๐ŸŽฌ Executing macro command...");
    this.commands.forEach(command => command.execute());
  }
  
  undo(): void {
    console.log("โ†ฉ๏ธ Undoing macro command...");
    // Undo in reverse order!
    [...this.commands].reverse().forEach(command => command.undo());
  }
}

// ๐Ÿ  Smart Home Controller
class SmartHomeController {
  private commands: Map<string, Command> = new Map();
  
  setCommand(name: string, command: Command): void {
    this.commands.set(name, command);
    console.log(`Command "${name}" programmed! ๐ŸŽฎ`);
  }
  
  executeCommand(name: string): void {
    const command = this.commands.get(name);
    if (command) {
      command.execute();
    } else {
      console.log(`Command "${name}" not found! ๐Ÿ˜•`);
    }
  }
  
  undoCommand(name: string): void {
    const command = this.commands.get(name);
    if (command) {
      command.undo();
    }
  }
}

// ๐ŸŽฎ Let's automate our home!
const tv = new Television();
const ac = new AirConditioner();
const security = new SecuritySystem();

const controller = new SmartHomeController();

// Individual commands
controller.setCommand("watchTV", new TVOnCommand(tv, 5));
controller.setCommand("coolRoom", new ACOnCommand(ac, 20));

// "Leaving Home" macro
const leavingHome = new MacroCommand([
  new TVOnCommand(tv, 1),    // Turn on TV to news channel
  new ACOnCommand(ac, 25),    // Set AC to eco mode
  new SecurityArmCommand(security)  // Arm security
]);

controller.setCommand("leaving", leavingHome);

// Execute the macro!
controller.executeCommand("leaving");
// ๐ŸŽฌ Executing macro command...
// TV is ON ๐Ÿ“บ
// Channel set to 1 ๐Ÿ“บ
// AC is ON โ„๏ธ
// Temperature set to 25ยฐC ๐ŸŒก๏ธ
// Security ARMED ๐Ÿ”’

// Oops, forgot something!
controller.undoCommand("leaving");
// โ†ฉ๏ธ Undoing macro command...
// Security DISARMED ๐Ÿ”“
// AC is OFF โ„๏ธ
// TV is OFF ๐Ÿ“บ

Example 3: Game Action System ๐ŸŽฎ

Letโ€™s create a game where player actions can be queued and replayed:

// ๐ŸŽฎ Game character
class GameCharacter {
  private x = 0;
  private y = 0;
  private health = 100;
  private inventory: string[] = [];
  
  move(dx: number, dy: number): void {
    this.x += dx;
    this.y += dy;
    console.log(`Moved to (${this.x}, ${this.y}) ๐Ÿƒ`);
  }
  
  takeDamage(amount: number): void {
    this.health -= amount;
    console.log(`Took ${amount} damage! Health: ${this.health} ๐Ÿ’”`);
  }
  
  heal(amount: number): void {
    this.health += amount;
    console.log(`Healed ${amount}! Health: ${this.health} ๐Ÿ’š`);
  }
  
  collectItem(item: string): void {
    this.inventory.push(item);
    console.log(`Collected ${item}! ๐ŸŽ`);
  }
  
  dropItem(item: string): void {
    const index = this.inventory.indexOf(item);
    if (index > -1) {
      this.inventory.splice(index, 1);
      console.log(`Dropped ${item}! ๐Ÿ—‘๏ธ`);
    }
  }
  
  getPosition(): { x: number; y: number } {
    return { x: this.x, y: this.y };
  }
}

// ๐ŸŽฏ Game commands
class MoveCommand implements Command {
  private previousX = 0;
  private previousY = 0;
  
  constructor(
    private character: GameCharacter,
    private dx: number,
    private dy: number
  ) {}
  
  execute(): void {
    const pos = this.character.getPosition();
    this.previousX = pos.x;
    this.previousY = pos.y;
    this.character.move(this.dx, this.dy);
  }
  
  undo(): void {
    const currentPos = this.character.getPosition();
    const dx = this.previousX - currentPos.x;
    const dy = this.previousY - currentPos.y;
    this.character.move(dx, dy);
  }
}

class CollectItemCommand implements Command {
  constructor(
    private character: GameCharacter,
    private item: string
  ) {}
  
  execute(): void {
    this.character.collectItem(this.item);
  }
  
  undo(): void {
    this.character.dropItem(this.item);
  }
}

// ๐ŸŽฌ Action Replay System
class ActionReplay {
  private actions: Command[] = [];
  private isRecording = false;
  
  startRecording(): void {
    this.isRecording = true;
    this.actions = [];
    console.log("๐Ÿ”ด Recording started!");
  }
  
  stopRecording(): void {
    this.isRecording = false;
    console.log("โน๏ธ Recording stopped!");
  }
  
  recordAction(command: Command): void {
    if (this.isRecording) {
      this.actions.push(command);
    }
    command.execute();
  }
  
  replay(): void {
    console.log("โ–ถ๏ธ Replaying actions...");
    this.actions.forEach((action, index) => {
      setTimeout(() => {
        console.log(`  Action ${index + 1}:`);
        action.execute();
      }, index * 1000); // Delay each action by 1 second
    });
  }
}

// ๐ŸŽฎ Let's play!
const player = new GameCharacter();
const replay = new ActionReplay();

// Start recording
replay.startRecording();

// Record some actions
replay.recordAction(new MoveCommand(player, 5, 0));      // Moved to (5, 0) ๐Ÿƒ
replay.recordAction(new CollectItemCommand(player, "๐Ÿ—ก๏ธ Sword"));  // Collected ๐Ÿ—ก๏ธ Sword! ๐ŸŽ
replay.recordAction(new MoveCommand(player, 0, 3));      // Moved to (5, 3) ๐Ÿƒ
replay.recordAction(new CollectItemCommand(player, "๐Ÿ›ก๏ธ Shield")); // Collected ๐Ÿ›ก๏ธ Shield! ๐ŸŽ

replay.stopRecording();

// Now replay the adventure!
console.log("\n๐ŸŽฌ Time to replay the adventure!");
replay.replay();

๐Ÿš€ Advanced Concepts

Command Queue with Priority ๐Ÿ“Š

// ๐ŸŽฏ Enhanced command with priority
interface PriorityCommand extends Command {
  priority: number;
  description: string;
}

class CommandQueue {
  private queue: PriorityCommand[] = [];
  private processing = false;
  
  add(command: PriorityCommand): void {
    this.queue.push(command);
    // Sort by priority (higher number = higher priority)
    this.queue.sort((a, b) => b.priority - a.priority);
    console.log(`๐Ÿ“ฅ Queued: ${command.description} (Priority: ${command.priority})`);
  }
  
  async processQueue(): Promise<void> {
    if (this.processing || this.queue.length === 0) return;
    
    this.processing = true;
    console.log("๐Ÿ”„ Processing command queue...");
    
    while (this.queue.length > 0) {
      const command = this.queue.shift()!;
      console.log(`โ–ถ๏ธ Executing: ${command.description}`);
      command.execute();
      
      // Simulate async processing
      await new Promise(resolve => setTimeout(resolve, 500));
    }
    
    this.processing = false;
    console.log("โœ… Queue processed!");
  }
}

// ๐ŸŽฎ Example usage
class DatabaseCommand implements PriorityCommand {
  constructor(
    public priority: number,
    public description: string,
    private action: () => void
  ) {}
  
  execute(): void {
    this.action();
  }
  
  undo(): void {
    console.log(`โ†ฉ๏ธ Undoing: ${this.description}`);
  }
}

const queue = new CommandQueue();

queue.add(new DatabaseCommand(1, "Save user profile", () => {
  console.log("๐Ÿ’พ Saving user profile...");
}));

queue.add(new DatabaseCommand(3, "Process payment", () => {
  console.log("๐Ÿ’ณ Processing payment...");
}));

queue.add(new DatabaseCommand(2, "Send email", () => {
  console.log("๐Ÿ“ง Sending email...");
}));

// Process in priority order
queue.processQueue();
// Output:
// ๐Ÿ”„ Processing command queue...
// โ–ถ๏ธ Executing: Process payment
// ๐Ÿ’ณ Processing payment...
// โ–ถ๏ธ Executing: Send email
// ๐Ÿ“ง Sending email...
// โ–ถ๏ธ Executing: Save user profile
// ๐Ÿ’พ Saving user profile...
// โœ… Queue processed!

Command with Validation and Rollback ๐Ÿ”„

// ๐ŸŽฏ Command with validation
interface ValidatedCommand extends Command {
  validate(): boolean;
  rollback(): void;
}

class TransactionCommand implements ValidatedCommand {
  private executed = false;
  
  constructor(
    private from: string,
    private to: string,
    private amount: number
  ) {}
  
  validate(): boolean {
    // Check if transaction is valid
    if (this.amount <= 0) {
      console.log("โŒ Invalid amount!");
      return false;
    }
    if (this.from === this.to) {
      console.log("โŒ Cannot transfer to same account!");
      return false;
    }
    console.log("โœ… Transaction validated!");
    return true;
  }
  
  execute(): void {
    if (!this.validate()) {
      throw new Error("Transaction validation failed!");
    }
    
    console.log(`๐Ÿ’ธ Transferring $${this.amount} from ${this.from} to ${this.to}`);
    this.executed = true;
  }
  
  undo(): void {
    if (this.executed) {
      console.log(`โ†ฉ๏ธ Reversing transaction: $${this.amount} from ${this.to} to ${this.from}`);
      this.executed = false;
    }
  }
  
  rollback(): void {
    console.log("๐Ÿ”„ Rolling back transaction...");
    this.undo();
  }
}

// ๐Ÿฆ Transaction processor
class TransactionProcessor {
  private completedTransactions: ValidatedCommand[] = [];
  
  async processTransaction(transaction: ValidatedCommand): Promise<void> {
    try {
      console.log("๐Ÿ”„ Processing transaction...");
      
      if (!transaction.validate()) {
        throw new Error("Validation failed!");
      }
      
      transaction.execute();
      this.completedTransactions.push(transaction);
      
      console.log("โœ… Transaction completed!");
    } catch (error) {
      console.log("โŒ Transaction failed!");
      transaction.rollback();
      throw error;
    }
  }
  
  rollbackAll(): void {
    console.log("๐Ÿšจ Rolling back all transactions!");
    [...this.completedTransactions].reverse().forEach(t => t.rollback());
    this.completedTransactions = [];
  }
}

โš ๏ธ Common Pitfalls and Solutions

โŒ Wrong: Commands with tight coupling

// โŒ Bad - Command knows too much about the receiver
class BadCommand implements Command {
  execute(): void {
    // Directly accessing internal implementation
    document.getElementById('button')!.style.color = 'red';
    window.localStorage.setItem('color', 'red');
    fetch('/api/save-color', { method: 'POST', body: 'red' });
  }
  
  undo(): void {
    // How do we undo all of this? ๐Ÿ˜ฑ
  }
}

โœ… Correct: Loosely coupled commands

// โœ… Good - Command delegates to receiver
class ColorChangeCommand implements Command {
  private previousColor: string = '';
  
  constructor(
    private colorService: ColorService,
    private newColor: string
  ) {}
  
  execute(): void {
    this.previousColor = this.colorService.getCurrentColor();
    this.colorService.setColor(this.newColor);
  }
  
  undo(): void {
    this.colorService.setColor(this.previousColor);
  }
}

// The service handles all the details
class ColorService {
  getCurrentColor(): string {
    return this.currentColor;
  }
  
  setColor(color: string): void {
    this.updateUI(color);
    this.saveToStorage(color);
    this.syncToServer(color);
    this.currentColor = color;
  }
  
  private currentColor = 'white';
  private updateUI(color: string): void { /* ... */ }
  private saveToStorage(color: string): void { /* ... */ }
  private syncToServer(color: string): void { /* ... */ }
}

โŒ Wrong: Forgetting to save state for undo

// โŒ Bad - Can't undo properly
class BadDeleteCommand implements Command {
  constructor(private list: string[], private index: number) {}
  
  execute(): void {
    this.list.splice(this.index, 1); // Lost the deleted item! ๐Ÿ˜ฑ
  }
  
  undo(): void {
    // What do we restore? ๐Ÿคท
  }
}

โœ… Correct: Saving state for undo

// โœ… Good - Saves state for undo
class GoodDeleteCommand implements Command {
  private deletedItem: string | undefined;
  
  constructor(private list: string[], private index: number) {}
  
  execute(): void {
    if (this.index >= 0 && this.index < this.list.length) {
      this.deletedItem = this.list[this.index];
      this.list.splice(this.index, 1);
    }
  }
  
  undo(): void {
    if (this.deletedItem !== undefined) {
      this.list.splice(this.index, 0, this.deletedItem);
    }
  }
}

๐Ÿ› ๏ธ Best Practices

  1. Keep Commands Simple and Focused ๐ŸŽฏ

    • Each command should do one thing well
    • Avoid complex logic in commands
    • Delegate actual work to receivers
  2. Always Implement Undo Properly โ†ฉ๏ธ

    • Save necessary state before execution
    • Test undo functionality thoroughly
    • Consider edge cases (what if undo is called twice?)
  3. Use Command Queues for Async Operations ๐Ÿ“‹

    • Queue commands for batch processing
    • Add priority support when needed
    • Handle failures gracefully
  4. Consider Macro Commands for Complex Operations ๐ŸŽญ

    • Group related commands together
    • Ensure proper undo order (usually reverse)
    • Test macro commands thoroughly
  5. Document Command Side Effects ๐Ÿ“

    • Clearly state what each command does
    • Document any external dependencies
    • Explain undo behavior

๐Ÿงช Hands-On Exercise

Create a drawing application that supports undo/redo for different shape operations! ๐ŸŽจ

Your challenge: Implement commands for drawing shapes with full undo/redo support.

// ๐ŸŽฏ Your task: Complete this drawing application!

interface Shape {
  id: string;
  type: 'circle' | 'rectangle' | 'line';
  x: number;
  y: number;
  color: string;
}

class DrawingCanvas {
  private shapes: Shape[] = [];
  private nextId = 1;
  
  addShape(type: Shape['type'], x: number, y: number, color: string): string {
    const id = `shape-${this.nextId++}`;
    const shape: Shape = { id, type, x, y, color };
    this.shapes.push(shape);
    console.log(`Drew ${color} ${type} at (${x}, ${y}) ๐ŸŽจ`);
    return id;
  }
  
  removeShape(id: string): Shape | undefined {
    const index = this.shapes.findIndex(s => s.id === id);
    if (index > -1) {
      const [removed] = this.shapes.splice(index, 1);
      console.log(`Removed ${removed.color} ${removed.type} ๐Ÿ—‘๏ธ`);
      return removed;
    }
    return undefined;
  }
  
  moveShape(id: string, newX: number, newY: number): { oldX: number; oldY: number } | undefined {
    const shape = this.shapes.find(s => s.id === id);
    if (shape) {
      const oldPos = { oldX: shape.x, oldY: shape.y };
      shape.x = newX;
      shape.y = newY;
      console.log(`Moved ${shape.type} to (${newX}, ${newY}) ๐Ÿƒ`);
      return oldPos;
    }
    return undefined;
  }
  
  getShapes(): Shape[] {
    return [...this.shapes];
  }
}

// TODO: Implement these commands!
class DrawShapeCommand implements Command {
  // Your code here! ๐ŸŽฏ
  execute(): void {
    // Implement drawing
  }
  
  undo(): void {
    // Implement undo
  }
}

class MoveShapeCommand implements Command {
  // Your code here! ๐ŸŽฏ
  execute(): void {
    // Implement moving
  }
  
  undo(): void {
    // Implement undo
  }
}

class DeleteShapeCommand implements Command {
  // Your code here! ๐ŸŽฏ
  execute(): void {
    // Implement deletion
  }
  
  undo(): void {
    // Implement undo
  }
}

// Test your implementation!
const canvas = new DrawingCanvas();
const history = new CommandHistory();

// Try drawing shapes and undoing/redoing! ๐ŸŽจ
๐Ÿ’ก Click here for the solution!
// ๐ŸŽฏ Solution: Complete drawing application with commands!

class DrawShapeCommand implements Command {
  private shapeId: string | null = null;
  
  constructor(
    private canvas: DrawingCanvas,
    private type: Shape['type'],
    private x: number,
    private y: number,
    private color: string
  ) {}
  
  execute(): void {
    this.shapeId = this.canvas.addShape(this.type, this.x, this.y, this.color);
  }
  
  undo(): void {
    if (this.shapeId) {
      this.canvas.removeShape(this.shapeId);
    }
  }
}

class MoveShapeCommand implements Command {
  private oldPosition: { oldX: number; oldY: number } | null = null;
  
  constructor(
    private canvas: DrawingCanvas,
    private shapeId: string,
    private newX: number,
    private newY: number
  ) {}
  
  execute(): void {
    this.oldPosition = this.canvas.moveShape(this.shapeId, this.newX, this.newY) || null;
  }
  
  undo(): void {
    if (this.oldPosition) {
      this.canvas.moveShape(this.shapeId, this.oldPosition.oldX, this.oldPosition.oldY);
    }
  }
}

class DeleteShapeCommand implements Command {
  private deletedShape: Shape | null = null;
  
  constructor(
    private canvas: DrawingCanvas,
    private shapeId: string
  ) {}
  
  execute(): void {
    this.deletedShape = this.canvas.removeShape(this.shapeId) || null;
  }
  
  undo(): void {
    if (this.deletedShape) {
      this.canvas.addShape(
        this.deletedShape.type,
        this.deletedShape.x,
        this.deletedShape.y,
        this.deletedShape.color
      );
    }
  }
}

// ๐ŸŽจ Let's test our drawing app!
const canvas = new DrawingCanvas();
const history = new CommandHistory();

// Draw some shapes
history.execute(new DrawShapeCommand(canvas, 'circle', 50, 50, 'red'));
history.execute(new DrawShapeCommand(canvas, 'rectangle', 100, 100, 'blue'));
history.execute(new DrawShapeCommand(canvas, 'line', 150, 150, 'green'));

console.log('\n๐Ÿ“Š Current shapes:', canvas.getShapes().length);

// Move a shape
const shapes = canvas.getShapes();
if (shapes[0]) {
  history.execute(new MoveShapeCommand(canvas, shapes[0].id, 75, 75));
}

// Undo the move
history.undo(); // โ†ฉ๏ธ Shape back to original position!

// Delete a shape
if (shapes[1]) {
  history.execute(new DeleteShapeCommand(canvas, shapes[1].id));
}

// Undo the delete
history.undo(); // โ†ฉ๏ธ Shape restored!

console.log('\n๐ŸŽ‰ Final shapes:', canvas.getShapes().length);

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered the Command Pattern! Hereโ€™s what youโ€™ve learned:

  • Encapsulation Power ๐Ÿ“ฆ: Commands wrap actions as objects
  • Undo/Redo Magic โ†ฉ๏ธ: Full control over action history
  • Queuing Excellence ๐Ÿ“‹: Process commands when ready
  • Macro Mastery ๐ŸŽญ: Combine simple commands into complex ones
  • Decoupling Benefits ๐Ÿ”—: Separate what from how

The Command Pattern is your Swiss Army knife for handling operations in TypeScript! Whether youโ€™re building text editors, game engines, or smart home systems, this pattern gives you incredible flexibility and control. ๐Ÿš€

๐Ÿค Next Steps

Congratulations on mastering the Command Pattern! ๐ŸŽ‰ Your TypeScript skills are reaching new heights!

Hereโ€™s what you can explore next:

  • Observer Pattern ๐Ÿ‘€: Learn how objects can watch and react to changes
  • Strategy Pattern ๐ŸŽฏ: Master interchangeable algorithms
  • Iterator Pattern ๐Ÿ”„: Navigate through collections like a pro

Keep practicing with the Command Pattern by:

  • Building an undo/redo system for a form ๐Ÿ“
  • Creating a macro recorder for automation ๐Ÿค–
  • Implementing a command-line interface ๐Ÿ’ป

Youโ€™re doing amazing! The Command Pattern is now part of your TypeScript toolkit! ๐ŸŽจโœจ