El panel de gestión del restaurante en PideYa es una botonera: aceptar pedido, marcar en preparación, avisar de retraso, cancelar. Cada botón hoy llama directamente a un método, la llamada se ejecuta y desaparece. Pero el restaurante pide cosas que una llamada efímera no puede dar: deshacer la última acción (el dedo resbala y cancela el pedido equivocado), una cola de operaciones cuando la cocina va saturada, un registro de quién hizo qué, y atajos que ejecutan varias acciones de golpe. Command resuelve todo eso con un movimiento: convertir cada petición en un objeto — y una vez que la petición es un objeto, se puede guardar, encolar, revertir y componer.

Contenido

  1. El problema en PideYa: acciones que se esfuman
  2. Intención y estructura del patrón
  3. Implementación Java completa
  4. Deshacer: el histórico de comandos
  5. Cola de comandos y macrocomandos
  6. Command en Java moderno: lambdas y Runnable
  7. Cuándo usarlo y cuándo no
  8. Relación con otros patrones
  9. Errores comunes
  10. Ejercicios y conclusión

El problema en PideYa: acciones que se esfuman

La versión actual del panel acopla cada botón a la lógica que ejecuta:

public class PanelGestionUI {
    public void onClickAceptar(Pedido pedido) {
        pedido.aceptar();
        notificador.enviar("Tu pedido ha sido aceptado");
    }
    public void onClickCancelar(Pedido pedido) {
        pedido.cancelar();
        pasarela.devolverCobro(pedido);
        notificador.enviar("Tu pedido ha sido cancelado");
    }
    // ... un método por botón, sin memoria de lo hecho
}

Lo que este diseño no puede hacer:

  • Deshacer: ejecutada la cancelación, no queda rastro de qué se hizo ni de cómo revertirlo.
  • Encolar: en hora punta el restaurante quiere aceptar pedidos y que se procesen por orden; una llamada directa se ejecuta ya o no se ejecuta.
  • Registrar/auditar: "¿quién canceló el pedido 4412 y cuándo?" exige esparcir logging por cada método.
  • Reutilizar la acción desde otro sitio (API, atajo de teclado, automatización "aceptar todo lo de menos de 20 €"): la lógica está soldada al botón.
  • Componer: el atajo "cerrar cocina" (rechazar pendientes + avisar a reparto + pausar la carta) no tiene dónde vivir.

El denominador común: la petición existe solo como llamada en la pila. Lo que necesitamos es cosificarla — que "cancelar el pedido 4412" sea un dato con el que trabajar.

Intención y estructura del patrón

Intención (GoF): encapsular una petición como un objeto, permitiendo parametrizar clientes con distintas peticiones, encolar o registrar peticiones, y soportar operaciones deshacibles.

Cuatro roles, y la clave está en quién ignora a quién: el Invoker (la botonera) ejecuta comandos sin saber qué hacen; el Receiver (el pedido, la pasarela) hace el trabajo sin saber quién lo pidió; el Command los une guardando todo lo necesario para ejecutar (y revertir) la petición.

classDiagram
    class Comando {
        <<interface>>
        +ejecutar()
        +deshacer()
        +descripcion() String
    }
    class ComandoAceptarPedido {
        -pedido: Pedido
        -notificador: Notificador
        +ejecutar()
        +deshacer()
    }
    class ComandoCancelarPedido {
        -pedido: Pedido
        -pasarela: PasarelaPago
        -notificador: Notificador
        +ejecutar()
        +deshacer()
    }
    class PanelGestion {
        -historico: Deque~Comando~
        +ejecutar(c: Comando)
        +deshacerUltimo()
    }
    class Pedido {
        +aceptar()
        +cancelar()
        +reabrir()
    }

    Comando <|.. ComandoAceptarPedido
    Comando <|.. ComandoCancelarPedido
    PanelGestion o-- Comando : histórico
    ComandoAceptarPedido --> Pedido : receiver
    ComandoCancelarPedido --> Pedido : receiver
Rol GoF En PideYa
Command (interfaz de la petición) Comando
ConcreteCommand ComandoAceptarPedido, ComandoCancelarPedido, ComandoMarcarEnPreparacion...
Invoker (dispara comandos, guarda histórico) PanelGestion
Receiver (quien hace el trabajo real) Pedido, PasarelaPago, Notificador
Client (crea el comando y fija su receiver) el código de la UI / configuración
sequenceDiagram
    participant UI as Botón cancelar
    participant P as PanelGestion (Invoker)
    participant C as ComandoCancelarPedido
    participant R as Pedido (Receiver)
    UI->>C: new ComandoCancelarPedido(pedido, ...)
    UI->>P: ejecutar(comando)
    P->>C: ejecutar()
    C->>R: cancelar()
    P->>P: histórico.push(comando)
    Note over P: más tarde...
    UI->>P: deshacerUltimo()
    P->>C: deshacer()
    C->>R: reabrir()

Implementación Java completa

La interfaz Command. Pequeña a propósito; descripcion() alimenta la auditoría y el rótulo "Deshacer cancelar pedido 4412" de la UI:

public interface Comando {
    void ejecutar();
    void deshacer();
    String descripcion();
}

Un comando concreto. Fíjate: recibe en el constructor todo lo que necesita (receiver incluido) — el comando es una llamada congelada, lista para disparar en cualquier momento y lugar:

public class ComandoCancelarPedido implements Comando {

    private final Pedido pedido;
    private final PasarelaPago pasarela;
    private final Notificador notificadorCliente;

    public ComandoCancelarPedido(Pedido pedido, PasarelaPago pasarela,
                                 Notificador notificadorCliente) {
        this.pedido = pedido;
        this.pasarela = pasarela;
        this.notificadorCliente = notificadorCliente;
    }

    @Override
    public void ejecutar() {
        pedido.cancelar();
        pasarela.devolverCobro(pedido.getId(), pedido.getTotal());
        notificadorCliente.enviar("Tu pedido " + pedido.getId() + " ha sido cancelado");
    }

    @Override
    public void deshacer() {
        pedido.reabrir();
        pasarela.cobrar(pedido.getId(), pedido.getTotal());
        notificadorCliente.enviar("Cancelación anulada: tu pedido " + pedido.getId() + " sigue en marcha");
    }

    @Override
    public String descripcion() {
        return "Cancelar pedido " + pedido.getId();
    }
}

ComandoAceptarPedido y ComandoMarcarEnPreparacion siguen el molde: ejecutar() avanza (pedido.aceptar(), pedido.marcarEnPreparacion()) y deshacer() retrocede (pedido.volverAPendiente(), pedido.volverAAceptado()). Qué transiciones son legales no es asunto del comando: eso lo gobernará State dentro de Pedido.

El Invoker. No sabe qué hace ningún comando; sabe ejecutarlos, recordarlos y auditarlos:

public class PanelGestion {

    private final Deque<Comando> historico = new ArrayDeque<>();

    public void ejecutar(Comando comando) {
        comando.ejecutar();
        historico.push(comando);
        RegistroEventos.INSTANCIA.info("Ejecutado: " + comando.descripcion());
    }

    public void deshacerUltimo() {
        if (historico.isEmpty()) {
            return;
        }
        Comando ultimo = historico.pop();
        ultimo.deshacer();
        RegistroEventos.INSTANCIA.info("Deshecho: " + ultimo.descripcion());
    }
}

El cliente monta y dispara:

panel.ejecutar(new ComandoCancelarPedido(pedido, pasarela, notificadorCliente));
// ¡ups, pedido equivocado!
panel.deshacerUltimo();

La botonera, la API pública y la automatización "aceptar pedidos pequeños" ahora ejecutan los mismos objetos comando: la acción vive en un solo sitio.

Deshacer: el histórico de comandos

El Deque como pila es la esencia del undo: LIFO — se deshace lo último primero. Tres decisiones de diseño que todo undo real debe tomar:

  1. ¿Deshacer por inversa o por instantánea? Nuestro deshacer() ejecuta la operación inversa (reabrir, recobrar). Funciona cuando la inversa existe y es fiable. Cuando no la hay (¿cuál es la inversa de "batir los huevos"?), la alternativa es guardar una foto del estado previo y restaurarla — eso es exactamente Memento, pareja clásica de Command, y allí lo desarrollamos.
  2. ¿Todo es deshacible? No: un pedido ya entregado no se "desentrega". Opciones: que deshacer() lance OperacionNoReversibleException, o una interfaz aparte ComandoReversible extends Comando y que el panel solo apile esos.
  3. ¿Histórico acotado? Un Deque sin límite es una fuga de memoria en un panel que corre semanas. Acótalo (p. ej., 50 entradas) descartando por el fondo.

Para rehacer (redo), añade una segunda pila: deshacerUltimo() mueve el comando a pilaRehacer; ejecutar un comando nuevo la vacía.

Cola de comandos y macrocomandos

Cola. Como el comando es un dato, ejecutar "ya" o "cuando toque" es solo cambiar el invoker: una BlockingQueue<Comando> donde la botonera deposita y un hilo trabajador drena. La cocina saturada procesa por orden, sin perder nada, y el productor no espera. (Este comando-que-viaja es el germen de los buses de mensajes y colas de tareas que veremos en el módulo 6.)

Macrocomando. Un comando cuyos hijos son comandos — la idea de Composite aplicada a las acciones:

public class MacroComando implements Comando {

    private final List<Comando> pasos;

    public MacroComando(String nombre, List<Comando> pasos) { /* ... */ }

    @Override
    public void ejecutar() {
        pasos.forEach(Comando::ejecutar);
    }

    @Override
    public void deshacer() {
        // ¡en orden inverso! Lo último hecho es lo primero deshecho
        for (int i = pasos.size() - 1; i >= 0; i--) {
            pasos.get(i).deshacer();
        }
    }
}

El atajo "cerrar cocina" es un MacroComando con tres pasos — y el panel lo ejecuta, apila y deshace exactamente igual que a un comando simple. Detalle con miga: si el paso 2 de 3 falla a mitad de ejecutar(), ¿deshaces los ya ejecutados? Eso es una compensación, y es la semilla del patrón Saga de microservicios (módulo 6).

Command en Java moderno: lambdas y Runnable

Runnable es, literalmente, la interfaz Command con un solo método y sin undo: ExecutorService.submit(Runnable) es un invoker con cola de comandos de serie en el JDK. Y si tu comando no necesita deshacer() ni descripcion(), una interfaz funcional y lambdas bastan:

@FunctionalInterface
public interface Accion { void ejecutar(); }

Map<String, Accion> atajos = Map.of(
    "aceptar",   () -> pedido.aceptar(),
    "preparando", () -> pedido.marcarEnPreparacion()
);
atajos.get("aceptar").ejecutar();

Es el mismo espíritu del RegistroNotificadores con Suppliers del Factory Method: la lambda captura receiver y argumentos, como el constructor del comando. La regla práctica: lambda para "ejecutar y olvidar"; clase cuando el comando tiene estado o contrato (undo, descripción, auditoría, serialización). Nuestro panel necesita las tres cosas: clases.

Cuándo usarlo y cuándo no

Úsalo cuando:

  • Necesitas deshacer/rehacer, histórico o auditoría de operaciones.
  • Quieres encolar, retrasar o programar peticiones (ejecutar en otro momento, otro hilo, otra máquina).
  • Quieres parametrizar objetos con acciones (botones, menús, atajos configurables) o componer operaciones (macros).
  • Necesitas desacoplar quién pide de quién hace y de cuándo se hace.

Evítalo cuando:

  • Es una llamada directa, síncrona, sin histórico ni variabilidad: pedido.aceptar() a secas es más claro que tres clases alrededor.
  • La "reversibilidad" es ilusoria (efectos externos irreversibles: emails enviados, cobros liquidados): un undo que no deshace de verdad es peor que no tenerlo.

Coste: una clase por operación (mitigable con lambdas para los casos simples) y la disciplina de mantener deshacer() como espejo fiel de ejecutar() — cada cambio en una debe reflejarse en la otra.

Relación con otros patrones

  • Memento: el undo por instantánea; Command+Memento es el dúo clásico del deshacer robusto.
  • Composite: el MacroComando es un composite de comandos.
  • Chain of Responsibility: los manejadores de la cadena pueden recibir comandos como petición.
  • Strategy: parecido superficial (objeto que encapsula código); intención distinta — Strategy encapsula cómo se hace algo; Command, que se pidió algo. Careo en la comparativa.
  • Prototype: comandos que se clonan como plantilla antes de ajustar parámetros.

Errores comunes

  • Comandos que hacen el trabajo ellos mismos en vez de delegar en receivers: el comando engorda hasta ser un "método con disfraz de clase" imposible de reutilizar. El comando coordina; el receiver hace.
  • deshacer() desincronizado de ejecutar(): se añade un efecto a la ejecución (una notificación, un cobro) y nadie toca la inversa. Testéalos en pareja: ejecutar(); deshacer(); debe dejar el sistema como estaba.
  • Histórico sin límite: fuga de memoria silenciosa (y con Memento dentro, grave).
  • Apilar comandos fallidos: si ejecutar() lanza excepción, no debe entrar al histórico (fíjate en que nuestro PanelGestion apila después de ejecutar, precisamente por esto).
  • Deshacer macros en el mismo orden que se ejecutaron: las inversas deben aplicarse en orden inverso, o las dependencias entre pasos se rompen.

Ejercicios

Ejercicio 1: comando de retraso

Implementa ComandoAvisarRetraso que suma minutos a la hora estimada del pedido (pedido.retrasarEntrega(minutos)) y notifica al cliente. Su deshacer() debe restar el retraso y avisar de que la hora vuelve a ser la original. ¿Qué guarda el comando para poder deshacer?

Ejercicio 2: rehacer

Amplía PanelGestion con rehacerUltimo() usando una segunda pila. Regla: ejecutar un comando nuevo invalida lo rehacible. Escribe la clase completa.

Ejercicio 3: ¿comando o lambda?

Para cada caso, decide si usarías una clase comando completa o una lambda/Runnable, y por qué: (a) refrescar la lista de pedidos al pulsar F5; (b) cancelar pedido con devolución de cobro; (c) los pasos internos del macro "cerrar cocina"; (d) reintentar el envío de una notificación en segundo plano.

Soluciones

Solución 1: el comando guarda los minutos que aplicó (su parámetro es a la vez la información de undo, porque la inversa es "restar lo sumado"):

public class ComandoAvisarRetraso implements Comando {

    private final Pedido pedido;
    private final Notificador notificadorCliente;
    private final int minutos;

    public ComandoAvisarRetraso(Pedido pedido, Notificador n, int minutos) {
        this.pedido = pedido;
        this.notificadorCliente = n;
        this.minutos = minutos;
    }

    @Override public void ejecutar() {
        pedido.retrasarEntrega(minutos);
        notificadorCliente.enviar("Tu pedido se retrasa " + minutos + " min. ¡Disculpa!");
    }

    @Override public void deshacer() {
        pedido.retrasarEntrega(-minutos);
        notificadorCliente.enviar("¡Buenas noticias! Tu pedido vuelve a la hora prevista");
    }

    @Override public String descripcion() {
        return "Retrasar pedido " + pedido.getId() + " (" + minutos + " min)";
    }
}

Si la inversa no fuera un simple -minutos (p. ej., si retrasar recalcula rutas), habría que guardar la hora estimada previa — y en cuanto el estado a guardar crece, estás pidiendo Memento.

Solución 2:

public class PanelGestion {

    private final Deque<Comando> pilaDeshacer = new ArrayDeque<>();
    private final Deque<Comando> pilaRehacer = new ArrayDeque<>();

    public void ejecutar(Comando comando) {
        comando.ejecutar();
        pilaDeshacer.push(comando);
        pilaRehacer.clear();               // lo nuevo invalida lo rehacible
    }

    public void deshacerUltimo() {
        if (pilaDeshacer.isEmpty()) return;
        Comando c = pilaDeshacer.pop();
        c.deshacer();
        pilaRehacer.push(c);
    }

    public void rehacerUltimo() {
        if (pilaRehacer.isEmpty()) return;
        Comando c = pilaRehacer.pop();
        c.ejecutar();
        pilaDeshacer.push(c);
    }
}

Solución 3: (a) lambda — sin estado, sin undo, pura acción de UI; (b) clase — necesita undo, descripción para auditoría y coordina varios receivers; (c) clases — el macro necesita poder deshacerlos en orden inverso, así que deben cumplir el contrato completo de Comando; (d) Runnable en un executor — es "ejecutar y olvidar" diferido; el reintento ya lo aporta el NotificadorConReintentos del módulo 3, no el comando.

Conclusión

Command reifica la petición: "cancelar el pedido 4412" ha dejado de ser una llamada fugaz para ser un objeto con ejecutar(), deshacer() y descripcion(), que el PanelGestion dispara, apila, audita y revierte sin saber qué hay dentro. De propina: colas de comandos para la hora punta, macrocomandos para los atajos compuestos, y la versión ligera con lambdas para las acciones sin contrato. Y quedó plantada la semilla de Memento: cuando la inversa no basta, se guarda una foto.

El próximo patrón también convierte algo en objetos, pero algo más exótico: frases de un lenguaje. Marketing quiere escribir reglas de promoción como "total > 30 Y dia == VIERNES → 10% de descuento" sin esperar un despliegue. Para evaluarlas necesitamos una gramática, un árbol de expresiones y un intérprete — y también necesitamos hablar con franqueza de por qué este patrón es el menos usado del catálogo. Nos vemos en Interpreter.

Curso de Patrones de Diseño de Software

Módulo 1: Introducción a los Patrones de Diseño

Módulo 2: Patrones Creacionales

Módulo 3: Patrones Estructurales

Módulo 4: Patrones de Comportamiento

Módulo 5: Aplicación de Patrones de Diseño

Módulo 6: Patrones de Diseño Avanzados

Módulo 7: Recursos Adicionales y Conclusión

© Copyright 2026. Todos los derechos reservados