20. Command (Comando) - Patrón de Comportamiento

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.

20.1 Problema y contexto

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.

20.2 Intención y motivación

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:

  • Se quiere parametrizar acciones de forma dinámica (por ejemplo, botones configurables o macros).
  • Se necesita registrar y reproducir operaciones, habilitando funciones de undo/redo.
  • Los comandos deben ejecutarse en otro momento o en otro proceso (colas, batch, remoting).
  • Se busca aplicar el principio Abierto/Cerrado para agregar nuevas acciones sin modificar emisores existentes.

La motivación central es convertir una petición en un objeto reutilizable y controlable, con comportamiento explícito y fácil de testear.

20.3 Participantes y estructura

Los elementos clásicos del patrón son:

  • Command: interfaz que declara el método para ejecutar la operación, y opcionalmente para deshacerla.
  • ConcreteCommand: implementaciones que delegan en un receptor y guardan el estado necesario para ejecutar/rehacer.
  • Receiver: componente que contiene la lógica real que el comando invoca.
  • Invoker: elemento que dispara el comando en respuesta a una petición del cliente.
  • Cliente: configura los comandos, asignándolos a invocadores y receptores.

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.

20.4 Clasificación de comandos

En la práctica aparecen varias categorías:

  • Comandos simples: encapsulan una llamada directa al receptor.
  • Macro comandos: agrupan otros comandos y los ejecutan secuencialmente, utiles para automatizar rutinas.
  • Comandos compuestos: ofrecen undo/redo ejecutando la inversa de cada subcomando.
  • Comandos diferidos: se almacenan en colas para ejecutarse posteriormente o bajo ciertas condiciones.

Esta clasificación ayuda a diseƱar catálogos de acciones flexibles, reusando estrategias de ejecución según la complejidad requerida.

20.5 Escenario: automatización de dispositivos inteligentes

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.

20.6 Implementación en C#

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));
        }
    }
}

20.7 Explicación del flujo

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.

20.8 Command en el ecosistema .NET

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.

20.9 Variantes y extensiones

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.

20.10 Riesgos y malas prácticas

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.

20.11 Buenas prácticas para aplicar Command

  • Diseñar comandos pequeños y orientados a una sola responsabilidad.
  • Registrar metadatos (timestamp, usuario, origen) para facilitar auditorías y reintentos.
  • Definir estrategias de limpieza del historial y límites de almacenamiento cuando el undo no es infinito.
  • Considerar la idempotencia de los comandos si se ejecutan en entornos distribuidos o con reintentos automáticos.

20.12 Relación con otros patrones

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.