StackPractices
intermediate Por Mathias Paulenko

Patrón Command

Encapsula una petición como un objeto, permitiendo parametrizar clientes con colas, logs y operaciones deshacibles. Patrón de diseño conductual.

Temas: design

Visión general

El Patrón Command es un patrón de diseño conductual que convierte una petición en un objeto independiente que contiene toda la información sobre la petición. Esto te permite parametrizar métodos con diferentes peticiones, retrasar o encolar ejecución y soportar operaciones deshacibles.

Es la base de sistemas de undo/redo, colas de trabajo, grabación de macros y operaciones transaccionales.

Cuándo usarlo

Usa el Patrón Command cuando:

  • Necesitas parametrizar objetos con operaciones a ejecutar
  • Quieres encolar, programar o ejecutar operaciones remotamente
  • Necesitas funcionalidad de deshacer/rehacer
  • Quieres registrar cambios para reproducción o auditoría
  • Necesitas comportamiento transaccional (ejecutar todo o revertir)

Solución

Python

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

    @abstractmethod
    def undo(self):
        pass

class Light:
    def __init__(self):
        self.is_on = False

    def turn_on(self):
        self.is_on = True
        print("Light is on")

    def turn_off(self):
        self.is_on = False
        print("Light is off")

class TurnOnCommand(Command):
    def __init__(self, light: Light):
        self.light = light

    def execute(self):
        self.light.turn_on()

    def undo(self):
        self.light.turn_off()

# Uso
light = Light()
cmd = TurnOnCommand(light)
cmd.execute()  # Light is on
cmd.undo()     # Light is off

JavaScript

class Light {
  constructor() {
    this.isOn = false;
  }
  turnOn() {
    this.isOn = true;
    console.log("Light is on");
  }
  turnOff() {
    this.isOn = false;
    console.log("Light is off");
  }
}

class TurnOnCommand {
  constructor(light) {
    this.light = light;
  }
  execute() {
    this.light.turnOn();
  }
  undo() {
    this.light.turnOff();
  }
}

// Uso
const light = new Light();
const cmd = new TurnOnCommand(light);
cmd.execute(); // Light is on
cmd.undo();    // Light is off

Java

interface Command {
    void execute();
    void undo();
}

class Light {
    boolean isOn = false;
    void turnOn() { isOn = true; System.out.println("Light is on"); }
    void turnOff() { isOn = false; System.out.println("Light is off"); }
}

class TurnOnCommand implements Command {
    private final Light light;
    TurnOnCommand(Light light) { this.light = light; }
    public void execute() { light.turnOn(); }
    public void undo() { light.turnOff(); }
}

// Uso
Light light = new Light();
Command cmd = new TurnOnCommand(light);
cmd.execute(); // Light is on
cmd.undo();    // Light is off

Explicación

El Patrón Command separa la invocación de la acción de su ejecución:

  • Interfaz Command: Declara execute() y opcionalmente undo()
  • Command concreto (TurnOnCommand): Vincula un receptor (Light) a una acción (turnOn)
  • Receptor (Light): El objeto que realiza el trabajo real
  • Invocador: Llama execute() en los commands (ej.

Al encapsular peticiones como objetos, ganas la habilidad de encolar, loggear y revertir operaciones.

Variantes

VarianteCaso de usoCompromiso
Command simpleAcción directa sin undoFácil de implementar, flexibilidad limitada
Command deshacibleOperaciones que pueden revertirseRequiere mantener estado para la reversión
Macro CommandCompuesto de múltiples commandsPotente, pero más difícil de deshacer atómicamente

Lo que funciona

  • Implementa undo() para cada command si tu sistema soporta deshacer
  • Mantén los commands sin estado cuando sea posible: Almacena estado del receptor, no del command
  • Usa un historial de commands (stack) para soportar deshacer/rehacer multinivel
  • Documenta efectos secundarios: Commands que afectan sistemas externos son más difíciles de deshacer
  • Considera inmutabilidad: Una vez configurado, un command no debería cambiar su target

Errores comunes

  • Olvidar estado de undo: Commands que no pueden revertirse rompen el stack de undo
  • Acoplamiento fuerte: Commands que dependen de estado global en lugar de un receptor específico
  • Sobre-ingeniería: Usar Command para operaciones triviales que nunca necesitan encolado o deshacer
  • Suposiciones síncronas: No considerar que los commands pueden ejecutarse asíncronamente
  • Falta de idempotencia: Ejecutar el mismo command dos veces produce resultados diferentes
  • No manejar fallos de command: Commands que lanzan excepciones pueden dejar el sistema en un estado inconsistente
  • Almacenar demasiado estado en commands: Los commands deberían ser ligeros; almacenar objetos grandes afecta memoria y serialización
  • Ignorar seguridad de threads: Commands ejecutados concurrentemente pueden acceder recursos compartidos sin sincronización apropiada
  • No loggear ejecución de command: Faltar trails de auditoría hace debugging y cumplimiento difícil
  • Mezclar preocupaciones: Commands que realizan múltiples responsabilidades no relacionadas violan el principio de responsabilidad única

Mejores Prácticas

  1. Mantén commands ligeros. Los commands deberían ser objetos pequeños y enfocados que encapsulan una sola acción. Evita almacenar grandes cantidades de datos en commands.

  2. Implementa undo para todos los commands que cambian estado. Si tu sistema soporta deshacer, cada command que cambie estado debería implementar lógica de undo.

  3. Usa factories de command para creación compleja. Cuando los commands requieren configuración compleja, usa métodos de factory o builders para encapsular lógica de creación.

  4. Maneja excepciones gracefulmente. Los commands deberían manejar sus propias excepciones o proporcionar información de error clara al invocador.

  5. Haz commands serializables. Si necesitas persistir commands o enviarlos por la red, asegúrate que puedan serializarse y deserializarse.

  6. Documenta efectos secundarios de command. Documenta claramente cualquier efecto secundario externo que un command pueda tener, especialmente aquellos difíciles de deshacer.

  7. Usa historial de command para debugging. Mantén un historial de commands ejecutados para ayudar con debugging y trails de auditoría.

  8. Considera composición de command. Usa commands compuestos para construir operaciones complejas de commands más simples y reutilizables.

  9. Valida parámetros de command. Valida parámetros de command antes de ejecución para fallar rápido y proporcionar mensajes de error claros.

  10. Prueba commands en aislamiento. Escribe pruebas unitarias para cada command independientemente, luego pruebas de integración para cadenas de command y macros.

Preguntas frecuentes

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

Strategy encapsula algoritmos intercambiables. Command encapsula una petición para realizar una acción, a menudo con soporte para deshacer, encolar y logging.

¿Se puede usar Command sin undo?

Sí. La capacidad de deshacer es opcional. Muchos sistemas usan Command únicamente para encolar y desacoplar invocadores de receptores.

¿Cómo implemento deshacer multinivel?

Mantén un stack de commands ejecutados. Deshacer hace pop del stack y llama undo(). Consulta Command con Undo/Redo para una implementación completa. Rehacer empuja el command de vuelta y llama execute().

¿Pueden los commands ejecutarse asíncronamente?

Sí. Implementa interfaces de command async con métodos execute() y undo() que retornan promesas o usan patrones async/await. Esto es útil para operaciones I/O-bound como llamadas a base de datos o peticiones de red.

¿Cómo manejo fallos de command?

Implementa manejo de errores a nivel de command o usa decoradores de command que envuelven ejecución con bloques try-catch. Considera implementar lógica de reintento, circuit breaking o mecanismos de fallback para fallos transientes.

¿Deberían los commands ser serializables?

Sí, si necesitas persistir commands para replay, trails de auditoría o ejecución distribuida. Asegúrate que los commands puedan serializarse a JSON, binario u otro formato y deserializarse sin perder estado.

¿Cómo implemento encolado de commands?

Usa una estructura de datos de cola para mantener commands y un hilo o proceso worker para ejecutarlos. Esto habilita ejecución diferida, procesamiento en background y balanceo de carga across workers.

¿Pueden los commands componerse?

Sí. Usa commands compuestos (macro commands) para combinar múltiples commands en una sola unidad ejecutable. Esto es útil para operaciones complejas que requieren múltiples pasos ejecutados atómicamente.

¿Cómo implemento logging de command?

Envuelve commands con decoradores de logging que registran detalles de ejecución, parámetros y resultados. Esto proporciona trails de auditoría e información de debugging para ejecución de command.

¿Deberían los commands validar sus parámetros?

Sí. Valida parámetros de command antes de ejecución para fallar rápido y proporcionar mensajes de error claros. Esto previene que commands inválidos se ejecuten y causen estado inconsistente.