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 Java

El siguiente ejemplo muestra una implementación con transiciones controladas y verificaciones específicas para cada estado:

package tutorial.state;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.Objects;

public interface EstadoPrestamo {
    void adjuntarDocumento(PrestamoContexto contexto, String documento);
    void evaluar(PrestamoContexto contexto);
    void aprobar(PrestamoContexto contexto, BigDecimal monto);
    void desembolsar(PrestamoContexto contexto);
    void rechazar(PrestamoContexto contexto, String motivo);
}

abstract class EstadoPrestamoBase implements EstadoPrestamo {
    protected void transicion(PrestamoContexto contexto, EstadoPrestamo nuevoEstado) {
        contexto.cambiarEstado(nuevoEstado);
    }

    @Override
    public void adjuntarDocumento(PrestamoContexto contexto, String documento) {
        throw new IllegalStateException("No se puede adjuntar documentos en este estado");
    }

    @Override
    public void evaluar(PrestamoContexto contexto) {
        throw new IllegalStateException("La evaluación no está disponible");
    }

    @Override
    public void aprobar(PrestamoContexto contexto, BigDecimal monto) {
        throw new IllegalStateException("No es posible aprobar desde este estado");
    }

    @Override
    public void desembolsar(PrestamoContexto contexto) {
        throw new IllegalStateException("No es posible desembolsar desde este estado");
    }

    @Override
    public void rechazar(PrestamoContexto contexto, String motivo) {
        throw new IllegalStateException("No se puede rechazar desde este estado");
    }
}

class EstadoCreado extends EstadoPrestamoBase {
    @Override
    public void adjuntarDocumento(PrestamoContexto contexto, String documento) {
        contexto.getDocumentos().add(Objects.requireNonNull(documento));
        System.out.println("Documento agregado: " + documento);
    }

    @Override
    public void evaluar(PrestamoContexto contexto) {
        if (contexto.getDocumentos().isEmpty()) {
            throw new IllegalStateException("Debe adjuntar al menos un documento");
        }
        System.out.println("Iniciando evaluación automática");
        transicion(contexto, new EstadoEnEvaluacion());
    }

    @Override
    public void rechazar(PrestamoContexto contexto, String motivo) {
        System.out.println("Solicitud rechazada en estado inicial: " + motivo);
        transicion(contexto, new EstadoRechazado());
    }
}

class EstadoEnEvaluacion extends EstadoPrestamoBase {
    @Override
    public void evaluar(PrestamoContexto contexto) {
        System.out.println("Evaluación en curso...");
    }

    @Override
    public void aprobar(PrestamoContexto contexto, BigDecimal monto) {
        if (monto.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("El monto debe ser positivo");
        }
        contexto.setMontoAprobado(monto);
        contexto.setFechaAprobacion(Instant.now());
        System.out.println("Préstamo aprobado por " + monto);
        transicion(contexto, new EstadoAprobado());
    }

    @Override
    public void rechazar(PrestamoContexto contexto, String motivo) {
        System.out.println("Evaluación rechazada: " + motivo);
        transicion(contexto, new EstadoRechazado());
    }
}

class EstadoAprobado extends EstadoPrestamoBase {
    @Override
    public void desembolsar(PrestamoContexto contexto) {
        if (contexto.getMontoAprobado() == null) {
            throw new IllegalStateException("No hay monto aprobado");
        }
        System.out.println("Desembolsando " + contexto.getMontoAprobado());
        contexto.setFechaDesembolso(Instant.now());
        transicion(contexto, new EstadoDesembolsado());
    }

    @Override
    public void rechazar(PrestamoContexto contexto, String motivo) {
        System.out.println("Solicitud rechazada luego de aprobación: " + motivo);
        transicion(contexto, new EstadoRechazado());
    }
}

class EstadoDesembolsado extends EstadoPrestamoBase {
    @Override
    public void desembolsar(PrestamoContexto contexto) {
        System.out.println("El préstamo ya fue desembolsado");
    }
}

class EstadoRechazado extends EstadoPrestamoBase {
    @Override
    public void rechazar(PrestamoContexto contexto, String motivo) {
        System.out.println("Ya se encuentra en estado rechazado");
    }
}

class PrestamoContexto {
    private EstadoPrestamo estadoActual;
    private java.util.List<String> documentos = new java.util.ArrayList<>();
    private BigDecimal montoAprobado;
    private Instant fechaAprobacion;
    private Instant fechaDesembolso;

    PrestamoContexto() {
        this.estadoActual = new EstadoCreado();
    }

    void cambiarEstado(EstadoPrestamo nuevoEstado) {
        this.estadoActual = Objects.requireNonNull(nuevoEstado);
    }

    public void adjuntarDocumento(String documento) {
        estadoActual.adjuntarDocumento(this, documento);
    }

    public void evaluar() {
        estadoActual.evaluar(this);
    }

    public void aprobar(BigDecimal monto) {
        estadoActual.aprobar(this, monto);
    }

    public void desembolsar() {
        estadoActual.desembolsar(this);
    }

    public void rechazar(String motivo) {
        estadoActual.rechazar(this, motivo);
    }

    java.util.List<String> getDocumentos() {
        return documentos;
    }

    void setMontoAprobado(BigDecimal monto) {
        this.montoAprobado = monto;
    }

    BigDecimal getMontoAprobado() {
        return montoAprobado;
    }

    void setFechaAprobacion(Instant fecha) {
        this.fechaAprobacion = fecha;
    }

    void setFechaDesembolso(Instant fecha) {
        this.fechaDesembolso = fecha;
    }
}

class AplicacionState {
    public static void main(String[] args) {
        PrestamoContexto prestamo = new PrestamoContexto();
        prestamo.adjuntarDocumento("DNI escaneado");
        prestamo.evaluar();
        prestamo.aprobar(new BigDecimal("150000"));
        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 transicion. 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.

23.8 State en el ecosistema Java

La documentación sobre key bindings de Swing describe cómo se gestionan acciones según distintos estados de foco y entrada. Frameworks de controladores de flujo, motores de workflow y bibliotecas de parsing (por ejemplo, analizadores lexicográficos) utilizan el patrón para representar máquinas de estados finitos con comportamiento modular.

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.