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 --> ReceiverImplementació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.