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.
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.
La intención es permitir que un objeto altere su comportamiento cuando cambia su estado interno. Se aplica cuando:
State promueve el principio Abierto/Cerrado al aislar el comportamiento en clases que pueden ampliarse sin tocar el contexto ni los estados existentes.
Los roles principales del patrón son:
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.
Existen dos formas comunes de administrar 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.
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.
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();
}
}
}
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.
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.
State puede combinarse con diferentes enfoques:
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.
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.