cv.cm

Programación · Patrones de diseño

Patrón Command explicado con ejemplos

5 min de lectura

Respuesta corta

Command es un patrón de diseño de comportamiento que convierte una petición en un objeto independiente que contiene todo lo necesario para ejecutarla. Como la petición es un objeto, puedes guardarla, pasarla de un lado a otro, ponerla en una cola, registrarla y deshacerla más tarde. El objeto que dispara un comando (por ejemplo un botón) no necesita saber qué objeto hace el trabajo ni cómo.

Intención y el problema que resuelve

Intención: encapsular una petición como un objeto, de modo que puedas parametrizar clientes con distintas peticiones, poner en cola o registrar peticiones y permitir operaciones que se pueden deshacer. Command es uno de los patrones de comportamiento del libro de la Banda de los Cuatro (Gang of Four, GoF).

Piensa en un editor de texto. La misma acción, como "poner el texto en negrita", puede dispararse desde un botón de la barra de herramientas, un elemento del menú y el atajo de teclado Ctrl+B. Si cada uno llama directamente al editor, la lógica se duplica en tres lugares. Peor aún, el editor también debe admitir deshacer: cuando el usuario pulsa Ctrl+Z, el programa necesita saber cuál fue la última acción y cómo revertirla.

Command resuelve ambos problemas. Cada acción se convierte en un objeto con un método execute y, opcionalmente, un método undo. Los botones, los menús y los atajos tienen todos el mismo objeto comando. El editor guarda una lista de historial de los comandos ejecutados, así que deshacer significa tomar el último comando de la lista y llamar a su método undo.

Estructura y participantes

RolResponsabilidad
ComandoInterfaz con un método execute (y a menudo undo)
Comando concretoGuarda el receptor y los parámetros, y llama a los métodos del receptor en execute
ReceptorEl objeto que sabe hacer el trabajo real, como un documento o una cuenta bancaria
InvocadorPide a un comando que se ejecute; puede guardar un historial para deshacer y rehacer
ClienteCrea comandos concretos y los conecta con receptores e invocadores

Implementación en TypeScript: deshacer y rehacer

El TextDocument es el receptor. InsertText y DeleteLast son comandos que recuerdan suficiente información para revertirse a sí mismos. El Editor es el invocador, con dos pilas: una para deshacer y otra para rehacer.

interface Command {
  execute(): void;
  undo(): void;
}
class TextDocument {
  text = "";
}
class InsertText implements Command {
  constructor(private doc: TextDocument, private value: string) {}
  execute() { this.doc.text += this.value; }
  undo() { this.doc.text = this.doc.text.slice(0, -this.value.length); }
}
class DeleteLast implements Command {
  private removed = "";
  constructor(private doc: TextDocument, private count: number) {}
  execute() {
    this.removed = this.doc.text.slice(-this.count);
    this.doc.text = this.doc.text.slice(0, -this.count);
  }
  undo() { this.doc.text += this.removed; }
}
class Editor {
  private done: Command[] = [];
  private undone: Command[] = [];
  run(cmd: Command) { cmd.execute(); this.done.push(cmd); this.undone = []; }
  undo() { const c = this.done.pop(); if (c) { c.undo(); this.undone.push(c); } }
  redo() { const c = this.undone.pop(); if (c) { c.execute(); this.done.push(c); } }
}
const doc = new TextDocument();
const editor = new Editor();
editor.run(new InsertText(doc, "Hello"));
editor.run(new InsertText(doc, ", world"));
editor.run(new DeleteLast(doc, 7));
console.log(doc.text); // Hello
editor.undo();
console.log(doc.text); // Hello, world
editor.redo();
console.log(doc.text); // Hello

Antes y después

Sin el patrón, la interfaz llama directamente al documento y deshacer no es posible, porque nada registra lo que ocurrió:

// Before: each trigger calls the receiver directly
boldButton.onclick = () => doc.makeBold(selection);
menuBold.onclick = () => doc.makeBold(selection);
// After: every trigger shares one command, and the editor records it
const bold = () => new MakeBold(doc, selection);
boldButton.onclick = () => editor.run(bold());
menuBold.onclick = () => editor.run(bold());

La segunda versión es un fragmento que supone una clase de comando MakeBold como las de arriba. La interfaz ya no sabe cómo funciona poner negrita, y cada acción pasa por un único lugar donde se puede registrar, anotar o deshacer.

Cuándo usarlo y cuándo no

  • Úsalo para deshacer y rehacer, macros (una lista de comandos ejecutados juntos) e historiales de comandos.
  • Úsalo para poner en cola o programar trabajo: una cola de trabajos guarda objetos comando y los workers los ejecutan más tarde.
  • Úsalo para registrar o reproducir operaciones, por ejemplo enviando comandos serializados a otro servidor.
  • Úsalo para desacoplar los elementos de la interfaz del código que disparan.
  • No lo uses cuando solo necesites un callback. En TypeScript una función ya es un valor de primera clase, así que button.onclick = save es un comando perfectamente válido sin deshacer. Introduce clases de comando cuando necesites comportamiento adicional como undo, un nombre para mostrar o serialización.

Usos en el mundo real

  • La interfaz Runnable de Java es un comando con un solo método, run, que usan los hilos y los ejecutores para ejecutar trabajo más tarde.
  • Las acciones de Redux son objetos simples que describen un cambio; un reducer las aplica, y el registro de acciones permite depurar viajando en el tiempo. Es un pariente cercano de Command, con la lógica en el reducer en lugar de en el comando.
  • VS Code y muchos otros editores registran comandos con nombre que pueden asociarse a teclas, menús y una paleta de comandos.
  • Las migraciones de bases de datos suelen tener un paso up y un paso down, que son el par execute y undo de un comando.

Patrones relacionados

PatrónEn qué se diferencia de Command
StrategyStrategy intercambia cómo se hace una tarea; Command empaqueta qué hacer (una petición) para poder guardarla, ponerla en cola o deshacerla.
MementoMemento guarda una instantánea del estado para deshacer; Command guarda la operación y la revierte. A menudo se combinan cuando revertir es difícil.
Chain of ResponsibilityUn comando puede pasarse por una cadena de manejadores para encontrar el que lo ejecuta.
CompositeUn comando macro es un Composite: un comando formado por otros comandos.
ObserverObserver notifica un evento a muchos oyentes; Command es una petición enviada a un receptor.

Preguntas frecuentes

¿Cómo implementa el patrón Command el deshacer?

Cada comando tiene un método undo que revierte su propio execute, a menudo usando datos guardados durante la ejecución. El invocador mantiene una pila de comandos ejecutados y llama a undo en el más reciente.

¿Cuál es la diferencia entre Command y Strategy?

Strategy representa distintas formas de hacer la misma tarea y la elige el objeto que la usa. Command representa una petición concreta, con su receptor y sus parámetros, que se puede ejecutar más tarde, poner en cola o deshacer.

¿Una función callback es un comando?

Un callback es un comando mínimo: empaqueta una acción que se ejecuta más tarde. Necesitas un objeto comando completo cuando además quieres deshacer, una descripción, serialización u otros metadatos.

¿Qué es un comando macro?

Un comando macro contiene una lista de comandos y los ejecuta en orden. Su deshacer los revierte en el orden contrario, lo que es una aplicación del patrón Composite.

Todos los tutoriales de programación