Command Pattern con Undo/Redo en TypeScript
Implementa el Command pattern para encapsular peticiones como objetos, habilitando operaciones undo/redo, encolamiento de peticiones y logging de operaciones
El Command pattern convierte una peticion en un objeto independiente que contiene toda la informacion sobre la peticion. Este desacoplamiento permite parametrizar metodos con diferentes peticiones, retrasar o encolar ejecucion, e implementar operaciones undo/redo — esencial para aplicaciones interactivas como editores, herramientas de dibujo y constructores de formularios.
Cuando Usar Esto
- Necesitas funcionalidad undo/redo en una interfaz de usuario
- Las operaciones deben ser encoladas, logueadas o ejecutadas remotamente
- El invocador no deberia saber cual receptor maneja una peticion
Problema
Un editor de texto llama directamente metodos en un objeto documento. Agregar undo requiere exponer estado interno, y agregar macros requiere duplicar logica en la capa de UI.
Solucion
// commands/Command.ts
interface Command {
execute(): void;
undo(): void;
getName(): string;
}
// Receiver
class TextDocument {
private content = '';
private history: string[] = [''];
insert(text: string, position: number): void {
this.content = this.content.slice(0, position) + text + this.content.slice(position);
this.saveState();
}
delete(position: number, length: number): string {
const removed = this.content.slice(position, position + length);
this.content = this.content.slice(0, position) + this.content.slice(position + length);
this.saveState();
return removed;
}
getContent(): string {
return this.content;
}
private saveState(): void {
this.history.push(this.content);
}
restoreState(index: number): void {
this.content = this.history[index] ?? this.content;
}
}
// Concrete Commands
class InsertCommand implements Command {
private previousLength: number;
constructor(
private document: TextDocument,
private text: string,
private position: number
) {
this.previousLength = document.getContent().length;
}
execute(): void {
this.document.insert(this.text, this.position);
}
undo(): void {
this.document.delete(this.position, this.text.length);
}
getName(): string {
return `Insert "${this.text}"`;
}
}
class DeleteCommand implements Command {
private deletedText: string = '';
constructor(
private document: TextDocument,
private position: number,
private length: number
) {}
execute(): void {
this.deletedText = this.document.delete(this.position, this.length);
}
undo(): void {
this.document.insert(this.deletedText, this.position);
}
getName(): string {
return `Delete ${this.length} chars`;
}
}
// Invoker
class CommandHistory {
private history: Command[] = [];
private currentIndex = -1;
execute(command: Command): void {
command.execute();
// Remover comandos redo
this.history = this.history.slice(0, this.currentIndex + 1);
this.history.push(command);
this.currentIndex++;
}
undo(): void {
if (this.currentIndex < 0) return;
this.history[this.currentIndex].undo();
this.currentIndex--;
}
redo(): void {
if (this.currentIndex >= this.history.length - 1) return;
this.currentIndex++;
this.history[this.currentIndex].execute();
}
canUndo(): boolean {
return this.currentIndex >= 0;
}
canRedo(): boolean {
return this.currentIndex < this.history.length - 1;
}
}
// Uso
const doc = new TextDocument();
const history = new CommandHistory();
history.execute(new InsertCommand(doc, 'Hello', 0));
history.execute(new InsertCommand(doc, ' World', 5));
console.log(doc.getContent()); // "Hello World"
history.undo();
console.log(doc.getContent()); // "Hello"
history.redo();
console.log(doc.getContent()); // "Hello World"
Variaciones
- Macro Command ejecuta multiples comandos como una unidad
- Async Command retorna una Promise para operaciones de larga duracion
- Composite Command trata un lote de comandos como una accion undoable
Consideraciones de Produccion
- Limita el tamano del historial para prevenir agotamiento de memoria en sesiones largas
- Serializa comandos a JSON para recuperacion de crash y edicion colaborativa
- Usa estados inmutables de documento para logica de undo mas simple en arquitecturas funcionales
Errores Comunes
- Almacenar snapshots completos del documento en lugar de operaciones inversas
- No manejar ejecucion concurrente de comandos en escenarios multi-usuario
- Olvidar limpiar la pila de redo cuando se ejecuta un nuevo comando despues de undo
Troubleshooting
- Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
- Tests break when the design changes: favor stable contracts over internal structure.
- Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
Temas Avanzados
Escenario: Sistema de Undo/Redo para Editor de Texto
// Command pattern: encapsular operaciones como objetos
interface Command {
execute(): void;
undo(): void;
describe(): string;
}
// Receptor: el editor de texto
class TextEditor {
private content = "";
private selection = { start: 0, end: 0 };
insert(text: string, pos: number) {
this.content = this.content.slice(0, pos) + text + this.content.slice(pos);
}
delete(start: number, end: number) {
this.content = this.content.slice(0, start) + this.content.slice(end);
}
getContent(): string { return this.content; }
}
// Comandos concretos
class InsertCommand implements Command {
constructor(private editor: TextEditor, private text: string, private pos: number) {}
execute() { this.editor.insert(this.text, this.pos); }
undo() { this.editor.delete(this.pos, this.pos + this.text.length); }
describe() { return `Insert "${this.text}" at ${this.pos}`; }
}
class DeleteCommand implements Command {
private deletedText = "";
constructor(private editor: TextEditor, private start: number, private end: number) {}
execute() {
this.deletedText = this.editor.getContent().slice(this.start, this.end);
this.editor.delete(this.start, this.end);
}
undo() { this.editor.insert(this.deletedText, this.start); }
describe() { return `Delete ${this.start}-${this.end}`; }
}
// Invocador: historial de comandos
class CommandHistory {
private undoStack: Command[] = [];
private redoStack: Command[] = [];
private maxHistory = 100;
execute(cmd: Command) {
cmd.execute();
this.undoStack.push(cmd);
if (this.undoStack.length > this.maxHistory) this.undoStack.shift();
this.redoStack = [];
}
undo(): Command | null {
const cmd = this.undoStack.pop();
if (cmd) { cmd.undo(); this.redoStack.push(cmd); }
return cmd;
}
redo(): Command | null {
const cmd = this.redoStack.pop();
if (cmd) { cmd.execute(); this.undoStack.push(cmd); }
return cmd;
}
canUndo(): boolean { return this.undoStack.length > 0; }
canRedo(): boolean { return this.redoStack.length > 0; }
}
// Uso
const editor = new TextEditor();
const history = new CommandHistory();
history.execute(new InsertCommand(editor, "Hello", 0));
history.execute(new InsertCommand(editor, " World", 5));
console.log(editor.getContent()); // "Hello World"
history.undo();
console.log(editor.getContent()); // "Hello"
history.redo();
console.log(editor.getContent()); // "Hello World"
Lecciones:
- Command encapsula operaciones como objetos con execute y undo
- El historial maneja undo/redo sin conocer detalles de cada comando
- Cada comando guarda el estado necesario para deshacer
- Limitar el historial (100 comandos) para evitar memory leaks
- Macro command: agrupar multiples comandos en uno solo
### Como implemento macros con Command?
Crea un MacroCommand que contiene una lista de comandos. Execute() llama a execute() de cada comando en orden. Undo() llama a undo() en orden inverso. Esto permite agrupar operaciones atomicas: por ejemplo, "formatear documento" ejecuta 20 comandos individuales, y un solo undo los revierte todos.
## Puntos Clave
- **Aplica command pattern con undo/redo en typescript** cuando necesites una solución práctica para tu caso de uso.
- **Monitorea el rendimiento** después de implementar; mide latencia, errores y uso de recursos antes y después.
- **Revisa la sección de Troubleshooting** ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- **Mantén dependencias actualizadas** y ejecuta tests en CI para prevenir regresiones en producción.
## Errores Comunes en Producción
- Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
- Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
- Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
- Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
- Ignorar modos de fallo que el patrón no cubre.
- Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
- Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
- Carecer de observabilidad sobre rendimiento y propagación de errores del patrón. Preguntas frecuentes
¿Es este patrón adecuado para proyectos pequeños?
Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.
¿Cómo se compara este patrón con alternativas?
Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.
¿Puedo aplicar este patrón parcialmente?
Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.
Recursos Relacionados
Memento Pattern para Snapshot y Restauracion de Estado
Captura y externaliza el estado interno de un objeto sin violar encapsulacion, habilitando undo, serializacion y rollback de estado en aplicaciones
PatternAbstract Factory para Familias de Componentes UI
Crea familias de objetos relacionados sin especificar clases concretas, habilitando implementaciones especificas de plataforma que comparten una interfaz comun
PatternDependency Injection Container en TypeScript
Construye un DI container liviano que resuelve dependencias de clases automaticamente, habilitando aplicaciones testeables y debilmente acopladas sin frameworks pesados
PatternInterpreter Pattern para Lenguajes de Expresion
Construye un interprete de lenguaje que evalua expresiones y reglas representando la gramatica como objetos componibles, util para formulas, queries y reglas de negocio
PatternVisitor Pattern para Operaciones Extensibles sobre
Separa algoritmos de los objetos sobre los que operan, permitiendo agregar nuevas operaciones sin modificar clases de elementos existentes