23. State (Estado) - Patrón de Comportamiento

El patrón State permite que un objeto altere su comportamiento cuando cambia su estado interno, simulando que ha cambiado de clase. Se basa en encapsular cada modo de operación en un objeto estado independiente y delegar en él las operaciones propias de ese modo.

State es especialmente útil en máquinas de estados con reglas complejas, flujos de aprobación, procesos de onboarding y cualquier escenario donde las transiciones determinan qué acciones están disponibles o cómo deben ejecutarse.

23.1 Problema y contexto

Los sistemas suelen tener componentes cuyo comportamiento depende de su estado actual: pedidos que cambian de disponibilidad, sprints en distintas etapas, dispositivos con modos operativos. Sin State, el código tiende a llenarse de condicionales encadenados (if/switch) para verificar el estado antes de ejecutar, lo que genera rigidez, dificulta la extensión y multiplica las posibilidades de error.

El patrón aborda el problema encapsulando cada estado en una clase, trasladando la lógica específica hacia el objeto estado. El contexto se limita a delegar según el estado activo y a coordinar las transiciones, reduciendo el acoplamiento y mejorando la claridad.

23.2 Intención y motivación

La intención es permitir que un objeto altere su comportamiento cuando cambia su estado interno. Se aplica cuando:

  • El comportamiento depende fuertemente de condiciones internas que cambian a lo largo del ciclo de vida.
  • Se debe evitar código con muchos condicionales que chequean el estado para decidir qué hacer.
  • Se requiere agregar nuevos estados o transiciones sin modificar el código existente.
  • Es necesario dar soporte a reglas de negocio complejas donde cada estado valida acciones específicas.

State promueve el principio Abierto/Cerrado al aislar el comportamiento en clases que pueden ampliarse sin tocar el contexto ni los estados existentes.

23.3 Participantes y estructura

Los roles principales del patrón son:

  • Context: objeto cuyo comportamiento varía según su estado actual. Mantiene una referencia al estado y delega en él las operaciones.
  • State: interfaz o clase abstracta que declara las operaciones disponibles para todos los estados.
  • ConcreteState: implementaciones concretas para cada estado que definen la lógica específica y, cuando corresponde, disparan transiciones a otros estados.

Algunas implementaciones incorporan un State Factory para centralizar la creación de estados y mantenerlos como singletons ligeros, optimizando memoria cuando existen muchos contextos similares.

23.4 Transiciones explícitas vs. implícitas

Existen dos formas comunes de administrar las transiciones:

  • Explícitas: el contexto decide y ejecuta el cambio de estado, manteniendo el control centralizado.
  • Implícitas: cada estado invoca al contexto para que cambie su referencia a otro estado, lo que simplifica la expresión de reglas complejas pero exige documentar claramente las transiciones.

La decisión depende de la complejidad del dominio. En ambos casos se recomienda registrar eventos o auditorías sobre los cambios para mantener trazabilidad.

23.5 Escenario: flujo de aprobación de un préstamo digital

Una entidad financiera ofrece préstamos a través de una app. El expediente pasa por estados: Creado, En Evaluación, Aprobado, Desembolsado y Rechazado. Cada estado admite acciones distintas: cargar documentación, ejecutar validaciones, liberar fondos o emitir notificaciones. Las reglas pueden cambiar por normativas y promociones temporales, por lo que se busca un diseño extensible.

State permite modelar cada etapa como un objeto independiente, encapsulando validaciones y transiciones permitidas. De esta forma, agregar un estado como En Revisión Manual requiere crear una nueva clase y conectar sus transiciones sin alterar el contexto.

23.6 Implementación en C#

El siguiente ejemplo muestra una implementación con transiciones controladas y verificaciones específicas para cada estado, aprovechando clases y colecciones de .NET:

using System;
using System.Collections.Generic;

namespace Tutorial.State
{
    public interface IEstadoPrestamo
    {
        void AdjuntarDocumento(PrestamoContexto contexto, string documento);
        void Evaluar(PrestamoContexto contexto);
        void Aprobar(PrestamoContexto contexto, decimal monto);
        void Desembolsar(PrestamoContexto contexto);
        void Rechazar(PrestamoContexto contexto, string motivo);
    }

    public abstract class EstadoPrestamoBase : IEstadoPrestamo
    {
        public virtual void AdjuntarDocumento(PrestamoContexto contexto, string documento)
        {
            LanzarOperacionNoPermitida(nameof(AdjuntarDocumento));
        }

        public virtual void Evaluar(PrestamoContexto contexto)
        {
            LanzarOperacionNoPermitida(nameof(Evaluar));
        }

        public virtual void Aprobar(PrestamoContexto contexto, decimal monto)
        {
            LanzarOperacionNoPermitida(nameof(Aprobar));
        }

        public virtual void Desembolsar(PrestamoContexto contexto)
        {
            LanzarOperacionNoPermitida(nameof(Desembolsar));
        }

        public virtual void Rechazar(PrestamoContexto contexto, string motivo)
        {
            LanzarOperacionNoPermitida(nameof(Rechazar));
        }

        protected static void Transicionar(PrestamoContexto contexto, IEstadoPrestamo nuevoEstado)
        {
            contexto.CambiarEstado(nuevoEstado);
        }

        protected static void LanzarOperacionNoPermitida(string operacion)
        {
            throw new InvalidOperationException($"La operacion {operacion} no esta disponible en el estado actual.");
        }
    }

    public sealed class EstadoCreado : EstadoPrestamoBase
    {
        public override void AdjuntarDocumento(PrestamoContexto contexto, string documento)
        {
            if (string.IsNullOrWhiteSpace(documento))
            {
                throw new ArgumentException("El documento no puede ser nulo o vacio.", nameof(documento));
            }

            contexto.AgregarDocumento(documento);
            Console.WriteLine($"Documento agregado: {documento}");
        }

        public override void Evaluar(PrestamoContexto contexto)
        {
            if (!contexto.TieneDocumentos)
            {
                throw new InvalidOperationException("Debe adjuntar al menos un documento.");
            }

            Console.WriteLine("Iniciando evaluacion automatica");
            Transicionar(contexto, new EstadoEnEvaluacion());
        }

        public override void Rechazar(PrestamoContexto contexto, string motivo)
        {
            Console.WriteLine($"Solicitud rechazada en estado inicial: {motivo}");
            Transicionar(contexto, new EstadoRechazado());
        }
    }

    public sealed class EstadoEnEvaluacion : EstadoPrestamoBase
    {
        public override void Evaluar(PrestamoContexto contexto)
        {
            Console.WriteLine("Evaluacion en curso...");
        }

        public override void Aprobar(PrestamoContexto contexto, decimal monto)
        {
            if (monto <= 0m)
            {
                throw new ArgumentOutOfRangeException(nameof(monto), "El monto debe ser positivo.");
            }

            contexto.EstablecerMontoAprobado(monto);
            contexto.EstablecerFechaAprobacion(DateTimeOffset.UtcNow);
            Console.WriteLine($"Prestamo aprobado por {monto:C2}");
            Transicionar(contexto, new EstadoAprobado());
        }

        public override void Rechazar(PrestamoContexto contexto, string motivo)
        {
            Console.WriteLine($"Evaluacion rechazada: {motivo}");
            Transicionar(contexto, new EstadoRechazado());
        }
    }

    public sealed class EstadoAprobado : EstadoPrestamoBase
    {
        public override void Desembolsar(PrestamoContexto contexto)
        {
            if (contexto.MontoAprobado is null)
            {
                throw new InvalidOperationException("No hay monto aprobado para desembolsar.");
            }

            Console.WriteLine($"Desembolsando {contexto.MontoAprobado:C2}");
            contexto.EstablecerFechaDesembolso(DateTimeOffset.UtcNow);
            Transicionar(contexto, new EstadoDesembolsado());
        }

        public override void Rechazar(PrestamoContexto contexto, string motivo)
        {
            Console.WriteLine($"Solicitud rechazada luego de la aprobacion: {motivo}");
            Transicionar(contexto, new EstadoRechazado());
        }
    }

    public sealed class EstadoDesembolsado : EstadoPrestamoBase
    {
        public override void Desembolsar(PrestamoContexto contexto)
        {
            Console.WriteLine("El prestamo ya fue desembolsado.");
        }
    }

    public sealed class EstadoRechazado : EstadoPrestamoBase
    {
        public override void Rechazar(PrestamoContexto contexto, string motivo)
        {
            Console.WriteLine("El prestamo ya se encuentra rechazado.");
        }
    }

    public sealed class PrestamoContexto
    {
        private IEstadoPrestamo _estadoActual;
        private readonly List<string> _documentos = new List<string>();

        public PrestamoContexto()
        {
            _estadoActual = new EstadoCreado();
        }

        public IReadOnlyCollection<string> Documentos => _documentos;
        public decimal? MontoAprobado { get; private set; }
        public DateTimeOffset? FechaAprobacion { get; private set; }
        public DateTimeOffset? FechaDesembolso { get; private set; }
        public bool TieneDocumentos => _documentos.Count > 0;

        internal void CambiarEstado(IEstadoPrestamo nuevoEstado)
        {
            _estadoActual = nuevoEstado ?? throw new ArgumentNullException(nameof(nuevoEstado));
        }

        internal void AgregarDocumento(string documento)
        {
            _documentos.Add(documento);
        }

        internal void EstablecerMontoAprobado(decimal monto)
        {
            MontoAprobado = monto;
        }

        internal void EstablecerFechaAprobacion(DateTimeOffset fecha)
        {
            FechaAprobacion = fecha;
        }

        internal void EstablecerFechaDesembolso(DateTimeOffset fecha)
        {
            FechaDesembolso = fecha;
        }

        public void AdjuntarDocumento(string documento)
        {
            _estadoActual.AdjuntarDocumento(this, documento);
        }

        public void Evaluar()
        {
            _estadoActual.Evaluar(this);
        }

        public void Aprobar(decimal monto)
        {
            _estadoActual.Aprobar(this, monto);
        }

        public void Desembolsar()
        {
            _estadoActual.Desembolsar(this);
        }

        public void Rechazar(string motivo)
        {
            _estadoActual.Rechazar(this, motivo);
        }
    }

    public static class AplicacionState
    {
        public static void Main()
        {
            var prestamo = new PrestamoContexto();
            prestamo.AdjuntarDocumento("DNI escaneado");
            prestamo.Evaluar();
            prestamo.Aprobar(150000m);
            prestamo.Desembolsar();
        }
    }
}

23.7 Explicación del flujo

El contexto PrestamoContexto delega todas las operaciones en el estado activo. Cada estado decide si admite la operación y, de ser necesario, dispara transiciones valiéndose del método Transicionar. El diseño minimiza la presencia de condicionales y facilita agregar nuevos estados si el flujo de negocio se expande.

El uso de una clase base con implementaciones por defecto evita duplicación y asegura mensajes claros cuando se intenta ejecutar una acción no permitida mediante excepciones consistentes.

23.8 State en el ecosistema .NET

La documentación de Windows Workflow Foundation muestra cómo modelar flujos declarativos basados en máquinas de estados que coordinan actividades y transiciones. Frameworks como Stateless o soluciones de orquestación como Durable Functions y Elsa Workflows adoptan el patrón para encapsular reglas de negocio complejas y permitir configuraciones externas de estados.

23.9 Variantes y extensiones

State puede combinarse con diferentes enfoques:

  • Estados compartidos: si los estados son inmutables se pueden reutilizar instancias (flyweights) para muchos contextos.
  • Estados jerárquicos: introducen herencia entre estados para reutilizar lógica (subestados).
  • State + Strategy: se usa para elegir estrategias de cálculo según el estado activo.
  • Modelado declarativo: las transiciones se definen en tablas o archivos de configuración, y el contexto carga estados dinámicamente.

23.10 Riesgos y malas prácticas

Un error común es permitir que estados y contexto se conozcan demasiado, generando acoplamientos bidireccionales complejos. También se debe evitar que los estados acumulen demasiada lógica compartida, ya que es preferible mantenerla en la clase base o en servicios auxiliares.

Otro riesgo es no documentar claramente las transiciones permitidas, lo que puede causar estados imposibles de alcanzar o bucles inadvertidos.

23.11 Buenas prácticas para aplicar State

  • Utilizar nombres de clases y métodos que reflejen el dominio para mejorar la comunicación con el equipo.
  • Registrar cada transición (por ejemplo, con eventos de dominio) para auditar el flujo.
  • Incorporar pruebas enfocadas en cada estado y en las combinaciones de transiciones para prevenir regresiones.
  • Considerar diagramas de estados como herramienta complementaria para validar el diseño con especialistas del negocio.

23.12 Relación con otros patrones

State se relaciona estrechamente con Strategy: ambos encapsulan comportamientos, pero State se enfoca en transiciones dependientes del contexto. Puede combinarse con Memento para guardar el estado previo y restaurarlo ante errores, y con Observer para notificar cambios de estado a otros componentes. Además, suele convivir con Abstract Factory para instanciar estados específicos según configuraciones.