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:
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();
}
}
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.
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.
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.