El patrón Command encapsula una petición como un objeto, permitiendo parametrizar emisores, encolar operaciones, registrar historiales y deshacer acciones. El comportamiento se separa de quien lo invoca, habilitando que una misma interfaz dispare comandos distintos según la configuración o el contexto.
Command es esencial en sistemas que requieren flexibilidad para combinar acciones: interfaces gráficas, automatización de tareas, orquestadores de flujos y motores de reglas. Al convertir cada operación en un objeto, es posible loguearla, reintentarla e incluso enviarla a procesos remotos.
Sin Command, los emisores (botones, menús, trabajos programados) conocen directamente al receptor y su implementación concreta. Esto genera acoplamientos rígidos, duplicación de lógica y dificulta la extensión. Además, almacenar historial para deshacer o reproducir acciones se vuelve complejo cuando los comandos no son objetos de primera clase.
El patrón soluciona el problema al encapsular la operación y sus datos en un objeto independiente. El emisor solo necesita una referencia a la interfaz comando para invocarla, mientras que el receptor permanece oculto y reemplazable.
La intención es desacoplar al emisor de la acción ejecutada, representando cada petición como un objeto que puede programarse, almacenarse y deshacerse. Command resulta pertinente cuando:
La motivación central es convertir una petición en un objeto reutilizable y controlable, con comportamiento explícito y fácil de testear.
Los elementos clásicos del patrón son:
Algunos diseños agregan un registro de historial que administra pilas de undo y redo, especialmente en aplicaciones de escritorio, editores o herramientas de diseño.
En la práctica aparecen varias categorías:
Esta clasificación ayuda a diseñar catálogos de acciones flexibles, reusando estrategias de ejecución según la complejidad requerida.
Imaginemos un hub domótico que centraliza luces, persianas y climatización. Los usuarios crean rutinas que combinan acciones (encender luces, ajustar temperatura, bajar persianas). Además, el sistema debe permitir deshacer la última rutina ejecutada y programar comandos para horarios futuros.
Command es ideal: cada acción se encapsula como un objeto que conoce a su dispositivo receptor. El hub actúa como invocador, registra el historial y admite que nuevas rutinas se construyan combinando comandos existentes o personalizados.
El siguiente ejemplo modela el escenario domótico con soporte para undo y macros:
package tutorial.command;
import java.time.LocalDateTime;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Objects;
public interface Comando {
void ejecutar();
void deshacer();
}
class DispositivoLuz {
private boolean encendida;
public void encender() {
encendida = true;
System.out.println("Luz encendida");
}
public void apagar() {
encendida = false;
System.out.println("Luz apagada");
}
}
class DispositivoClima {
private int temperatura = 22;
public void ajustar(int nuevaTemperatura) {
System.out.println("Clima ajustado a " + nuevaTemperatura + "°C");
this.temperatura = nuevaTemperatura;
}
public int temperaturaActual() {
return temperatura;
}
}
class ComandoEncenderLuz implements Comando {
private final DispositivoLuz luz;
ComandoEncenderLuz(DispositivoLuz luz) {
this.luz = Objects.requireNonNull(luz);
}
@Override
public void ejecutar() {
luz.encender();
}
@Override
public void deshacer() {
luz.apagar();
}
}
class ComandoAjustarClima implements Comando {
private final DispositivoClima clima;
private final int temperaturaObjetivo;
private int temperaturaAnterior;
ComandoAjustarClima(DispositivoClima clima, int temperaturaObjetivo) {
this.clima = Objects.requireNonNull(clima);
this.temperaturaObjetivo = temperaturaObjetivo;
}
@Override
public void ejecutar() {
temperaturaAnterior = clima.temperaturaActual();
clima.ajustar(temperaturaObjetivo);
}
@Override
public void deshacer() {
clima.ajustar(temperaturaAnterior);
}
}
class MacroComando implements Comando {
private final List<Comando> comandos = new ArrayList<>();
public MacroComando agregar(Comando comando) {
comandos.add(comando);
return this;
}
@Override
public void ejecutar() {
for (Comando comando : comandos) {
comando.ejecutar();
}
}
@Override
public void deshacer() {
for (int i = comandos.size() - 1; i >= 0; i--) {
comandos.get(i).deshacer();
}
}
}
class RegistroComandos {
private final Deque<Comando> historial = new ArrayDeque<>();
public void registrar(Comando comando) {
historial.push(comando);
}
public void deshacerUltimo() {
if (!historial.isEmpty()) {
Comando ultimo = historial.pop();
ultimo.deshacer();
} else {
System.out.println("No hay comandos para deshacer");
}
}
}
class HubDomotico {
private final RegistroComandos registro = new RegistroComandos();
public void ejecutarComando(Comando comando) {
comando.ejecutar();
registro.registrar(comando);
}
public void deshacer() {
registro.deshacerUltimo();
}
public void programar(Comando comando, LocalDateTime momento) {
System.out.printf("Comando programado para %s%n", momento);
ejecutarComando(comando);
}
}
class AplicacionCommand {
public static void main(String[] args) {
DispositivoLuz luzSala = new DispositivoLuz();
DispositivoClima climaSala = new DispositivoClima();
Comando encenderLuz = new ComandoEncenderLuz(luzSala);
Comando ajustarClima = new ComandoAjustarClima(climaSala, 24);
MacroComando rutinaBienvenida = new MacroComando()
.agregar(encenderLuz)
.agregar(ajustarClima);
HubDomotico hub = new HubDomotico();
hub.ejecutarComando(rutinaBienvenida);
hub.deshacer();
hub.programar(encenderLuz, LocalDateTime.now().plusHours(3));
}
}
El hub actúa como invocador: recibe comandos, los ejecuta y registra para permitir deshacer. Cada comando conoce a su receptor y almacena el estado necesario para revertir la operación. MacroComando
demuestra cómo componer comandos simples en rutinas complejas conservando la capacidad de undo.
El método programar
ilustra cómo tratar a los comandos como unidades independientes que pueden planificarse o enviarse a otra cola sin modificar al emisor.
Las bibliotecas gráficas utilizan Command para desacoplar eventos de interfaz de la lógica de negocio. La documentación oficial de javax.swing.Action explica cómo encapsular acciones reutilizables que pueden asociarse a botones, menús y atajos de teclado. De forma similar, Runnable
y Callable
actúan como comandos genéricos ejecutados por ExecutorService
.
Command puede combinarse con patrones de mensajería para distribuir acciones a través de colas. Algunas extensiones frecuentes incluyen registros persistentes de eventos, comandos transaccionales que se compensan ante fallos y comandos seguros que validan permisos antes de ejecutar.
En arquitecturas orientadas a dominios, Command se relaciona con CQRS, donde las operaciones de escritura se encapsulan como comandos y se procesan mediante buses especializados.
Un riesgo habitual es crear comandos excesivamente generales que conocen demasiados detalles del sistema, perdiendo cohesión. También puede aparecer un consumo de memoria elevado si el historial guarda objetos pesados sin mecanismos de limpieza. En entornos concurrentes se debe garantizar que los receptores sean thread-safe o que los comandos serialicen el acceso.
Command se integra con Memento para guardar el estado previo antes de ejecutar y simplificar undo. Puede combinarse con Composite para implementar macros de comandos y con Chain of Responsibility para filtrar o transformar peticiones antes de llegar al receptor. Además, colabora con Observer cuando la ejecución de un comando debe notificar a subsistemas interesados.