Saltar al contenidoJMRG
Todas las entradas

Patrón Command: Acciones como Objetos

design-patternsbehavioralcommand

Intención

Command encapsula una solicitud como un objeto, permitiendo parametrizar clientes con diferentes solicitudes, encolar o registrar solicitudes, y soportar operaciones reversibles (undo/redo).

El Problema

Imagina un editor de texto. Sin Command, el código de undo sería un desastre:

// Sin patrón: estado global y lógica dispersa
let previousContent: string;

function bold() {
  previousContent = content;
  content = `<b>${content}</b>`;
}

function undo() {
  content = previousContent; // ¿Y si hay múltiples undos?
}

La Solución: Command

classDiagram
    class Command {
        <<interface>>
        +execute(): void
        +undo(): void
    }

    class Invoker {
        -history: Command[]
        +executeCommand(cmd: Command)
        +undoLast()
    }

    class ConcreteCommand {
        -receiver: Receiver
        -state
        +execute(): void
        +undo(): void
    }

    Command <|.. ConcreteCommand
    Invoker --> Command
    ConcreteCommand --> Receiver

Implementación TypeScript

// Interfaz Command
interface Command {
  execute(): void;
  undo(): void;
  getDescription(): string;
}

// Receiver: el objeto que realiza las acciones
class TextDocument {
  private content = '';

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

  insertAt(position: number, text: string): void {
    this.content = this.content.slice(0, position) + text + this.content.slice(position);
  }

  deleteRange(start: number, end: number): string {
    const deleted = this.content.slice(start, end);
    this.content = this.content.slice(0, start) + this.content.slice(end);
    return deleted;
  }
}

// Comando concreto
class InsertTextCommand implements Command {
  constructor(
    private document: TextDocument,
    private position: number,
    private text: string
  ) {}

  execute(): void {
    this.document.insertAt(this.position, this.text);
  }

  undo(): void {
    this.document.deleteRange(this.position, this.position + this.text.length);
  }

  getDescription(): string {
    return `Insert "${this.text.substring(0, 20)}"`;
  }
}

// Invoker con historial
class TextEditor {
  private history: Command[] = [];
  private redoStack: Command[] = [];
  private document = new TextDocument();

  type(text: string): void {
    const cmd = new InsertTextCommand(
      this.document,
      this.document.getContent().length,
      text
    );
    this.executeCommand(cmd);
  }

  private executeCommand(cmd: Command): void {
    cmd.execute();
    this.history.push(cmd);
    this.redoStack = [];
  }

  undo(): boolean {
    const cmd = this.history.pop();
    if (!cmd) return false;
    cmd.undo();
    this.redoStack.push(cmd);
    return true;
  }

  redo(): boolean {
    const cmd = this.redoStack.pop();
    if (!cmd) return false;
    cmd.execute();
    this.history.push(cmd);
    return true;
  }

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

// Uso
const editor = new TextEditor();
editor.type('Hello');
editor.type(' World');
console.log(editor.getContent()); // "Hello World"

editor.undo();
console.log(editor.getContent()); // "Hello"

editor.redo();
console.log(editor.getContent()); // "Hello World"

Macro Commands

class MacroCommand implements Command {
  private commands: Command[] = [];

  add(cmd: Command): this {
    this.commands.push(cmd);
    return this;
  }

  execute(): void {
    for (const cmd of this.commands) {
      cmd.execute();
    }
  }

  undo(): void {
    // Undo en orden inverso
    for (let i = this.commands.length - 1; i >= 0; i--) {
      this.commands[i].undo();
    }
  }
}

Cuándo Usar

Situación Recomendación
Undo/Redo de operaciones ✅ Recomendado
Colas de tareas ✅ Recomendado
Transacciones ✅ Recomendado
Operaciones simples sin historial ❌ Excesivo

Conclusión

Command transforma operaciones en objetos de primera clase, habilitando undo, colas, y composición.