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, aprovechando colecciones genéricas de .NET y verificaciones de nulidad:
using System;
using System.Collections.Generic;
namespace Tutorial.Command
{
public interface IComando
{
void Ejecutar();
void Deshacer();
}
public sealed class DispositivoLuz
{
public bool EstaEncendida { get; private set; }
public void Encender()
{
EstaEncendida = true;
Console.WriteLine("Luz encendida");
}
public void Apagar()
{
EstaEncendida = false;
Console.WriteLine("Luz apagada");
}
}
public sealed class DispositivoClima
{
public int TemperaturaActual { get; private set; } = 22;
public void AjustarTemperatura(int temperatura)
{
TemperaturaActual = temperatura;
Console.WriteLine($"Clima ajustado a {temperatura} grados");
}
}
public sealed class ComandoEncenderLuz : IComando
{
private readonly DispositivoLuz _luz;
private bool _estadoPrevio;
public ComandoEncenderLuz(DispositivoLuz luz)
{
_luz = luz ?? throw new ArgumentNullException(nameof(luz));
}
public void Ejecutar()
{
_estadoPrevio = _luz.EstaEncendida;
_luz.Encender();
}
public void Deshacer()
{
if (!_estadoPrevio)
{
_luz.Apagar();
}
}
}
public sealed class ComandoAjustarClima : IComando
{
private readonly DispositivoClima _clima;
private readonly int _temperaturaObjetivo;
private int _temperaturaAnterior;
public ComandoAjustarClima(DispositivoClima clima, int temperaturaObjetivo)
{
_clima = clima ?? throw new ArgumentNullException(nameof(clima));
_temperaturaObjetivo = temperaturaObjetivo;
}
public void Ejecutar()
{
_temperaturaAnterior = _clima.TemperaturaActual;
_clima.AjustarTemperatura(_temperaturaObjetivo);
}
public void Deshacer()
{
_clima.AjustarTemperatura(_temperaturaAnterior);
}
}
public sealed class MacroComando : IComando
{
private readonly List<IComando> _comandos = new List<IComando>();
public MacroComando Agregar(IComando comando)
{
if (comando == null) throw new ArgumentNullException(nameof(comando));
_comandos.Add(comando);
return this;
}
public void Ejecutar()
{
foreach (var comando in _comandos)
{
comando.Ejecutar();
}
}
public void Deshacer()
{
for (var i = _comandos.Count - 1; i >= 0; i--)
{
_comandos[i].Deshacer();
}
}
}
public sealed class RegistroComandos
{
private readonly Stack<IComando> _historial = new Stack<IComando>();
public void Registrar(IComando comando)
{
if (comando == null) throw new ArgumentNullException(nameof(comando));
_historial.Push(comando);
}
public void DeshacerUltimo()
{
if (_historial.Count == 0)
{
Console.WriteLine("No hay comandos para deshacer");
return;
}
var comando = _historial.Pop();
comando.Deshacer();
}
}
public sealed class HubDomotico
{
private readonly RegistroComandos _registro = new RegistroComandos();
public void EjecutarComando(IComando comando)
{
if (comando == null) throw new ArgumentNullException(nameof(comando));
comando.Ejecutar();
_registro.Registrar(comando);
}
public void Deshacer()
{
_registro.DeshacerUltimo();
}
public void Programar(IComando comando, DateTime momento)
{
Console.WriteLine($"Comando programado para {momento:dd/MM/yyyy HH:mm}");
EjecutarComando(comando);
}
}
public static class AplicacionCommand
{
public static void Main()
{
var luzSala = new DispositivoLuz();
var climaSala = new DispositivoClima();
IComando encenderLuz = new ComandoEncenderLuz(luzSala);
IComando ajustarClima = new ComandoAjustarClima(climaSala, 24);
IComando rutinaBienvenida = new MacroComando()
.Agregar(encenderLuz)
.Agregar(ajustarClima);
var hub = new HubDomotico();
hub.EjecutarComando(rutinaBienvenida);
hub.Deshacer();
hub.Programar(encenderLuz, DateTime.Now.AddHours(3));
}
}
}
El hub actúa como invocador: recibe comandos, los ejecuta y los registra en un Stack<IComando>
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 aplicaciones de escritorio y XAML utilizan System.Windows.Input.ICommand para encapsular acciones reutilizables asignadas a botones, atajos y controles de interfaz. En ASP.NET Core o servicios de dominio es habitual modelar las operaciones como comandos que viajan por buses mediadores (por ejemplo, MediatR). Delegados como Action
y Func<T>
, así como tareas Task
, proporcionan comandos genéricos que pueden ejecutarse de forma concurrente o diferida mediante planificadores.
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.