State Pattern
Intención y problema
¿Cómo permitir que un objeto cambie su comportamiento cuando cambia su estado interno, evitando condiciones dispersas? State organiza operaciones mediante colaboradores que representan el estado actual y las reglas relevantes de su lifecycle. Pertenece a behavioral-patterns.
La intención GoF de variar comportamiento con el estado interno se recoge en la síntesis de CMU sobre State. El ejemplo de órdenes siguiente es propio; sus transiciones ilustran una política, no reglas universales de comercio.
Order admite agregar productos mientras está Draft, confirmar bajo ciertas condiciones y enviar cuando está Confirmed. Cuando cada método repite comprobaciones de Status, esas reglas pueden divergir. La presión útil para State es comportamiento significativo ligado al lifecycle, no la mera existencia de un enum.
Forces, invariantes y estructura
Queremos localizar comportamiento específico sin perder una vista comprensible de las transiciones. También queremos conservar invariantes comunes de Order y evitar que cualquier caller pueda asignar arbitrariamente un estado.
Order Context
↓ delegates
OrderState state-dependent contract
├── Draft
├── Confirmed
├── Shipped
└── CancelledContext mantiene identidad, datos y estado actual. La abstracción State expresa las operaciones cuyo comportamiento depende de ese estado. Los estados concretos organizan lo admitido, lo rechazado y, según diseño, la decisión de transición.
Order puede delegar Confirm a Draft, que verifica condiciones relevantes y permite pasar a Confirmed. En Shipped, esa misma operación debe tener una respuesta definida. No hace falta que todos los métodos pertenezcan a State: el comportamiento independiente del lifecycle puede permanecer en Context.
Delegar no elimina ownership. Si cada estado escribe cualquier campo interno del Context, las invariantes quedan expuestas. Es importante definir qué capacidades necesita y qué cambios pueden realizarse conjuntamente.
Colaboración y grafo de transición
Para nuestra política simplificada:
Draft ──Confirm──→ Confirmed ──Ship──→ Shipped
│ │
└────Cancel──────────┴────────────→ Cancelled| Estado y operación | Resultado bajo esta política |
|---|---|
| Draft + AddItem | Permanece Draft; valida la modificación. |
| Draft + Confirm | Pasa a Confirmed si la orden está completa. |
| Confirmed + Ship | Pasa a Shipped bajo condiciones de envío. |
| Draft o Confirmed + Cancel | Pasa a Cancelled si se admite cancelación. |
| Shipped + ReopenAsDraft | Transición inválida. |
| Cancelled + Confirm | Operación rechazada. |
El gráfico debe ser visible aunque la implementación distribuya reglas. El nombre Shipped no impide por sí solo volver a Draft: necesita una política que controle esa transición.
También debe definirse cuándo cambia el estado respecto de otros efectos. Asignar Shipped antes de saber si una acción necesaria tuvo éxito puede dejar una representación engañosa. Un patrón local no hace atómicos cambios de memoria, persistencia y operaciones externas.
command-pattern puede representar la petición Confirm o Cancel, mientras State determina comportamiento según el estado actual. Diferir una petición no conserva automáticamente las condiciones de cuando se creó.
Quién decide el siguiente estado
| Variante | Beneficio posible | Costo |
|---|---|---|
| Context decide la transición. | Centraliza el grafo y puede mantener estados más simples. | Separa algunas decisiones del comportamiento específico. |
| State decide el próximo State. | Acerca reglas y acciones propias de un estado. | Puede acoplar estados entre sí y dispersar el grafo. |
En la primera variante, un estado puede devolver un resultado significativo y Context aplicar una transición admitida. En la segunda, Draft puede solicitar pasar a Confirmed mediante una capacidad controlada del Context.
Ninguna es universalmente superior. Hay que poder identificar autoridad y consecuencias sin recorrer todas las clases. Mezclar decisiones entre Context y States sin una regla consistente puede duplicar transiciones o permitir caminos distintos para la misma operación.
Transición centralizada no significa un setter público para cualquier estado. Transición decidida por State tampoco exige conocer todas las clases concretas siguientes: contratos y ensamblaje pueden limitar esa dependencia cuando la necesidad lo justifica.
Transiciones inválidas
Una operación inadmisible necesita una respuesta contractual: rechazo mediante resultado explícito, excepción o no operation si ese comportamiento está deliberadamente definido. Ignorar silenciosamente una transición imposible puede hacer que el caller crea que ocurrió.
Por ejemplo, ReopenAsDraft sobre Shipped debería comunicar que no se admite en nuestro modelo. Una repetición de una operación ya completada puede tener otra política, pero debe distinguirse de una transición inválida. Ser tolerante a repetición no autoriza alterar el lifecycle.
Las pruebas deberían cubrir operaciones admitidas y rechazadas, preservación de invariantes y ausencia de cambios parciales ante failure. Probar solamente cuál clase State está activa no demuestra que el objeto se comporta correctamente.
State frente a enum y switch
Un enum y un pequeño switch pueden ser más claros cuando hay pocos estados, pocas operaciones y reglas estables. Un flag no invalida el diseño; una clase por cada valor no lo mejora automáticamente.
State puede ayudar cuando muchas operaciones dependen del estado, crecen comportamientos sustancialmente distintos, se duplican condiciones o nuevas reglas evolucionan con independencia. pattern-selection mantiene esa evaluación frente a una tabla o función directa.
El patrón agrega tipos, delegación, lifecycle de estados y navegación. Si cada clase contiene una sola condición trivial, puede ocultar una tabla que cabía en una pantalla. La extensión tampoco siempre es local: un estado nuevo puede modificar transiciones y reglas de varios estados existentes.
State y Strategy
strategy-pattern selecciona comportamiento intercambiable para una tarea; State organiza comportamiento asociado al estado actual y sus transiciones. La pregunta práctica es quién o qué causa el cambio.
Una política de precios puede seleccionarse por configuración o caller. Draft cambia a Confirmed como consecuencia del lifecycle de Order. La selección externa de una Strategy y la transición interna de State son tendencias útiles, no una regla matemática: lo decisivo es la intención de la colaboración.
Ambos pueden dibujarse Context → interface → implementación. Conservar datos en una estrategia no la convierte en State. Un UML parecido no reemplaza analizar problema, fuerzas y consecuencias.
State Pattern, State Machine y workflow
Una State Machine es un modelo más general de estados y transiciones. Puede expresarse mediante switch, tabla, grafo, biblioteca o modelo declarativo; State Pattern es una organización OO posible del comportamiento. State Pattern no equivale a State Machine.
La especificación SCXML del W3C ejemplifica una formulación declarativa de estados y transiciones, distinta de exigir una jerarquía State. No se necesita adoptar esa tecnología para reconocer la diferencia entre modelo y realización.
Un workflow puede agregar persistencia, timers, eventos externos, retries, múltiples actores y ejecución de larga duración. State Pattern no es un workflow engine ni proporciona recuperación después de reiniciar un proceso. El lifecycle representado y la durabilidad de su ejecución son obligaciones diferentes.
Alternativas modernas, límites y misuses
Enums con pattern matching, tagged unions, tablas y tipos que representan estados pueden reducir ceremony. El Rust Book contrasta State con una alternativa que codifica estados mediante tipos. Puede impedir ciertas operaciones inválidas en compilación, a cambio de otra API y restricciones.
Estados sin datos propios pueden compartirse si sus operaciones lo permiten; estados con información específica requieren ownership adecuado. Una representación compartida mutable puede mezclar órdenes. El patrón tampoco resuelve por sí solo transiciones concurrentes, sincronización ni conflictos de persistencia.
Son misuses crear clases por enums triviales, esconder el grafo, introducir dependencias circulares difíciles de seguir y repartir autoridad inconsistentemente. También lo es aceptar transiciones imposibles sin comunicar resultado.
State ayuda cuando la organización hace explícitos comportamiento e invariantes del lifecycle. design-patterns permite contrastar las familias; conservar una tabla pequeña puede ser una mejor decisión que materializar una colaboración sin presión suficiente.