Patrón State
Permite que un objeto altere su comportamiento cuando cambia su estado interno. El patrón de comportamiento GoF para máquinas de estados finitos.
Visión General
Tu clase Order empieza pequeña: pay(), ship(), cancel(). Luego a cada método le crece un switch (status). Pagar solo vale mientras está pendiente, enviar solo después del pago, cancelar salvo que ya se haya enviado. Seis meses después el mismo switch está copiado en todos los métodos, y añadir un estado refunded obliga a auditar cada uno.
El patrón State es la solución clásica. Es uno de los 11 patrones de comportamiento del libro Design Patterns de la Gang of Four (Gamma et al., 1994): cada estado se convierte en su propia clase, el contexto delega las llamadas al objeto de estado actual, y son los estados los que deciden las transiciones legales. Añadir un estado es añadir una clase, no releer cada condicional.
Dos aclaraciones rápidas antes de empezar:
- Esto es el patrón de objetos GoF: una forma de modelar una máquina de estados finitos con clases. No trata de gestión de estado en UI: para componentes React y statecharts estilo XState, consulta el Patrón State Machine para UI.
- Si tu máquina tiene dos o tres estados con comportamiento trivial, un
switcho un enum es suficiente. El patrón se justifica cuando las transiciones y la lógica por estado se complican.
Cuándo Usarlo
Usa el Patrón State cuando:
- El comportamiento de un objeto depende de su estado y debe cambiar en tiempo de ejecución: un
Orderque solo se envía tras el pago, unaConnectionque se comporta distinto abierta que cerrada, un reproductor que ignoraplay()mientras está detenido. - La misma comprobación de estado está copiada en todos los métodos: la señal clásica de una máquina de estados que fuga condicionales.
- Los estados tienen transiciones con efectos secundarios: acciones de entrada/salida como registro, notificaciones o liberación de recursos.
- Aparecen nuevos estados con frecuencia, y cada uno debería aterrizar como clase nueva sin tocar las existentes (principio abierto/cerrado).
No lo uses cuando la máquina es degenerada (dos estados, transiciones sin comportamiento) o cuando una tabla de transiciones dirigida por datos (ver Variantes) ya cubre lo que necesitas. Un enum con switch no es un delito; el problema llega cuando el mismo switch está duplicado.
Problema
Esta es la forma que el patrón elimina: cada método repitiendo las mismas comprobaciones de estado:
class Order:
def __init__(self):
self.status = "pending"
def pay(self):
if self.status == "pending":
print("Procesando pago...")
self.status = "paid"
else:
print(f"No se puede pagar en estado {self.status}")
def ship(self):
if self.status == "paid":
print("Enviando pedido...")
self.status = "shipped"
else:
print(f"No se puede enviar en estado {self.status}")
# cancel() repite la misma escalera de if/elif,
# y "refunded" habría que añadirlo a los tres.
La superficie de bugs crece con cada estado: las transiciones se validan en tres sitios, los mensajes de error divergen, y refunded obliga a editar todos los métodos. El patrón State mueve cada rama del if a una clase de estado donde vive una sola vez.
Solución
El mismo Order reconstruido con objetos de estado: cada clase de estado posee su comportamiento y sus transiciones de salida:
Python
from abc import ABC, abstractmethod
class OrderState(ABC):
@abstractmethod
def pay(self, order):
pass
@abstractmethod
def ship(self, order):
pass
@abstractmethod
def cancel(self, order):
pass
class PendingState(OrderState):
def pay(self, order):
print("Procesando pago...")
order.transition_to(PaidState())
def ship(self, order):
print("No se puede enviar: pago pendiente")
def cancel(self, order):
print("Pedido cancelado")
order.transition_to(CancelledState())
class PaidState(OrderState):
def pay(self, order):
print("Ya pagado")
def ship(self, order):
print("Enviando pedido...")
order.transition_to(ShippedState())
def cancel(self, order):
print("Emitiendo reembolso...")
order.transition_to(CancelledState())
class ShippedState(OrderState):
def pay(self, order):
print("Ya pagado")
def ship(self, order):
print("Ya enviado")
def cancel(self, order):
print("No se puede cancelar: ya enviado")
class CancelledState(OrderState):
def pay(self, order):
print("No se puede pagar: pedido cancelado")
def ship(self, order):
print("No se puede enviar: pedido cancelado")
def cancel(self, order):
print("Ya cancelado")
class Order:
def __init__(self):
self._state = PendingState()
def transition_to(self, state):
self._state = state
def pay(self):
self._state.pay(self)
def ship(self):
self._state.ship(self)
def cancel(self):
self._state.cancel(self)
# Uso
order = Order()
order.pay()
order.ship()
order.cancel() # No se puede cancelar: ya enviado
JavaScript
class Order {
constructor() { this.state = new PendingState(this); }
transitionTo(state) { this.state = state; }
pay() { this.state.pay(); }
ship() { this.state.ship(); }
cancel() { this.state.cancel(); }
}
class PendingState {
constructor(order) { this.order = order; }
pay() {
console.log("Procesando pago...");
this.order.transitionTo(new PaidState(this.order));
}
ship() { console.log("No se puede enviar: pago pendiente"); }
cancel() {
console.log("Pedido cancelado");
this.order.transitionTo(new CancelledState(this.order));
}
}
class PaidState {
constructor(order) { this.order = order; }
pay() { console.log("Ya pagado"); }
ship() {
console.log("Enviando pedido...");
this.order.transitionTo(new ShippedState(this.order));
}
cancel() {
console.log("Emitiendo reembolso...");
this.order.transitionTo(new CancelledState(this.order));
}
}
class ShippedState {
constructor(order) { this.order = order; }
pay() { console.log("Ya pagado"); }
ship() { console.log("Ya enviado"); }
cancel() { console.log("No se puede cancelar: ya enviado"); }
}
class CancelledState {
constructor(order) { this.order = order; }
pay() { console.log("No se puede pagar: pedido cancelado"); }
ship() { console.log("No se puede enviar: pedido cancelado"); }
cancel() { console.log("Ya cancelado"); }
}
const order = new Order();
order.pay();
order.ship();
order.cancel(); // No se puede cancelar: ya enviado
Java
interface OrderState {
void pay(Order order);
void ship(Order order);
void cancel(Order order);
}
class PendingState implements OrderState {
public void pay(Order order) {
System.out.println("Procesando pago...");
order.transitionTo(new PaidState());
}
public void ship(Order order) {
System.out.println("No se puede enviar: pago pendiente");
}
public void cancel(Order order) {
System.out.println("Pedido cancelado");
order.transitionTo(new CancelledState());
}
}
class PaidState implements OrderState {
public void pay(Order order) {
System.out.println("Ya pagado");
}
public void ship(Order order) {
System.out.println("Enviando pedido...");
order.transitionTo(new ShippedState());
}
public void cancel(Order order) {
System.out.println("Emitiendo reembolso...");
order.transitionTo(new CancelledState());
}
}
class ShippedState implements OrderState {
public void pay(Order order) {
System.out.println("Ya pagado");
}
public void ship(Order order) {
System.out.println("Ya enviado");
}
public void cancel(Order order) {
System.out.println("No se puede cancelar: ya enviado");
}
}
class CancelledState implements OrderState {
public void pay(Order order) {
System.out.println("No se puede pagar: pedido cancelado");
}
public void ship(Order order) {
System.out.println("No se puede enviar: pedido cancelado");
}
public void cancel(Order order) {
System.out.println("Ya cancelado");
}
}
class Order {
private OrderState state = new PendingState();
public void transitionTo(OrderState state) {
this.state = state;
}
public void pay() { state.pay(this); }
public void ship() { state.ship(this); }
public void cancel() { state.cancel(this); }
}
// Uso
Order order = new Order();
order.pay();
order.ship();
order.cancel(); // No se puede cancelar: ya enviado
Explicación
El patrón tiene tres roles:
- Contexto (
Order): mantiene una referencia al estado actual y le delega cada llamada dependiente del estado. No sabe qué transiciones son legales. - Interfaz de estado (
OrderState): el contrato que implementa cada estado: un método por evento al que responde la máquina. - Estados concretos (
PendingState,PaidState,ShippedState,CancelledState): implementan el comportamiento de su estado y ejecutan las transiciones cambiando la referencia de estado del contexto.
El libro de GoF lo llama “Objects for States” y su ejemplo canónico es TCPConnection: un socket cuyos open(), acknowledge() y close() se comportan distinto según su estado sea Listen, Established o Closed. La misma idea que nuestro Order, a nivel de protocolo.
¿Quién posee las transiciones?
La versión clásica de GoF (la del ejemplo) deja que los estados decidan el siguiente estado. Hay que ser honesto con los compromisos:
- Transiciones en el estado (este ejemplo): cada clase de estado conoce a sus sucesores. Las transiciones viven junto al comportamiento, pero los estados concretos quedan acoplados entre sí;
PendingStateno compila sinPaidState. - Transiciones en el contexto: el
Orderconsulta una tabla de transiciones y los estados no se referencian entre sí. Los estados se vuelven reutilizables entre máquinas, pero el contexto recupera la lógica condicional, justo lo que queríamos eliminar. - Dirigida por tabla: las transiciones viven en datos (
transitions[state][event] = next), no en clases. Mejor cuando hay muchos estados con comportamiento escaso (ver Variantes).
Acciones de entrada y salida
Cuando las transiciones necesitan efectos secundarios (registros de auditoría, métricas, liberar un recurso), ponlos en hooks enter()/exit() de la interfaz de estado, y haz que transition_to los invoque:
def transition_to(self, state):
if self._state:
self._state.exit(self)
self._state = state
self._state.enter(self)
enter() corre tras el cambio, exit() antes. Ahí es donde un pedido pendiente registra “salió de pending tras 3 días” o un estado pagado dispara el correo de “pago confirmado”: efectos que pertenecen a la transición, no a quien llamó a pay().
Variantes
| Variante | Descripción | Caso de Uso |
|---|---|---|
| Máquina de Estados Simple | Los estados manejan transiciones | Pequeño número de estados |
| Máquina de Estados Jerárquica | Los estados pueden tener subestados | Comportamiento anidado complejo |
| Tabla de Estados | Las transiciones se almacenan en tabla de búsqueda | Muchos estados con transiciones predecibles |
| Autómata de Pila | Historial de estado basado en pila | Transiciones reversibles (deshacer). Consulta Memento |
Para la variante dirigida por tabla, la máquina entera colapsa a un dict; merece la pena en cuanto pasas de unos seis estados y la lógica por estado se queda delgada:
TRANSITIONS = {
("pending", "pay"): "paid",
("paid", "ship"): "shipped",
("pending", "cancel"): "cancelled",
("paid", "cancel"): "cancelled",
}
class InvalidTransition(Exception):
pass
class Order:
def __init__(self):
self.state = "pending"
def handle(self, event):
next_state = TRANSITIONS.get((self.state, event))
if next_state is None:
raise InvalidTransition(f"{event} no permitido en {self.state}")
self.state = next_state
Esta versión mantiene la matriz de transiciones válidas en un solo sitio legible (puedes hasta renderizarla en la documentación), pero pierdes los hooks por estado y tienes que colgar los efectos secundarios en otro lugar. Para máquinas de verdad grandes, librerías como transitions (Python), xstate (TypeScript) o Spring StateMachine ya hacen esto y añaden guardas, jerarquías y visualización.
Cómo Probar Máquinas de Estados
El patrón se paga solo a la hora de probarlo: cada estado es una unidad y las transiciones son aserciones sobre el contexto:
def test_pedido_pendiente_pasa_a_pagado():
order = Order()
order.pay()
assert isinstance(order._state, PaidState)
def test_no_se_puede_enviar_antes_de_pagar():
order = Order()
order.ship()
assert isinstance(order._state, PendingState)
def test_pedido_cancelado_rechaza_todo():
order = Order()
order.cancel()
order.pay()
assert isinstance(order._state, CancelledState)
Prueba la matriz, no el cableado: un test por cada par (estado, evento) que importe, más los estados terminales. El camino de InvalidTransition en la variante de tabla es un buen candidato para property-based testing: cualquier par fuera de TRANSITIONS debe lanzar la excepción.
Lo que funciona
- Delega todo el comportamiento dependiente del estado en objetos de estado, manteniendo el contexto ligero
- Haz los objetos de estado inmutables o recréalos en cada transición para evitar bugs de estado compartido
- Usa enums para estados bien definidos en lenguajes que los soportan (Java, TypeScript)
- Añade hooks de entrada/salida cuando las transiciones necesiten efectos secundarios (registro, analítica)
- Considera una tabla de transiciones cuando tengas muchos estados y las transiciones se vuelvan difíciles de gestionar
Errores Comunes
- Transiciones inválidas silenciosas. Devolver
nullo registrarlo en silencio ante un evento ilegal esconde bugs; lanzaInvalidTransitiono al menos haz que el no-op sea ruidoso. - Compartir una única instancia de estado entre contextos. Si
PaidStatetiene campos mutables, dos pedidos comparten bugs. Instancia por pedido o mantén los estados sin estado interno. - Lógica de negocio dentro del contexto. Que
Ordercrezca conif state == "paid": apply_discount()es el problema original regresando: el descuento pertenece aPaidStateo a un colaborador. - Estados que llaman a métodos del contexto ajenos. Si un estado empieza a necesitar
order.user.emailyorder.inventory.reserve(), la señal es que necesitas el Mediator o un servicio de dominio, no más acoplamiento. - Un contexto que expone
transition_topúblicamente. Que el llamador hagaorder.transition_to(ShippedState())se salta todas las reglas; mantén las transiciones disparadas solo por eventos. (En los ejemplos de arriba,transition_toexiste para uso interno de los estados; piénsalo como fontanería, no API pública._transition_totambién vale.)
Ver También
- Patrón Command — encapsula peticiones como objetos; combina natural con State cuando las transiciones disparan comandos
- Patrón Observer — para notificar a los suscriptores cuando ocurre una transición
- Patrón Memento — captura el estado para deshacer; complementa la variante de autómata de pila
- Patrón State Machine para UI — el hermano de front-end: máquinas de estados finitos y statecharts para componentes UI
- Repositorio companion — la máquina de estados del pedido como archivos ejecutables en Python, JavaScript y Java, incluida la variante dirigida por tabla
Lectura Recomendada
- Refactoring.Guru — State — diagramas UML y un ejemplo de edición de documentos
- Design Patterns: Elements of Reusable Object-Oriented Software (GoF) — la definición original (Gamma, Helm, Johnson, Vlissides, 1994)
- Librería Python transitions — máquinas de estados dirigidas por tabla con callbacks, cuando las clases se hacen pesadas
- Documentación de XState — statecharts para TypeScript y aplicaciones UI
Preguntas frecuentes
¿Cuál es la diferencia entre State y Strategy?
Ambos encapsulan comportamiento en objetos intercambiables, pero la mecánica difiere. State cambia lo que hace el objeto a medida que su estado interno avanza por un ciclo de vida. Nadie elige nada. Strategy funciona al revés: el llamador elige el algoritmo desde fuera y al objeto le da igual cuál le tocó.
¿Los estados deben conocerse entre sí?
En la implementación clásica, los estados disparan transiciones a otros estados. Una alternativa es dejar que el contexto o una tabla de transiciones gestione las transiciones, manteniendo los estados desacoplados.
¿Puedo usar enums en lugar de clases para máquinas de estado simples?
Sí. Para el caso simple, un enum más un switch hace el trabajo. Recurre al patrón completo cuando los estados cargan comportamiento real o esperes seguir añadiéndolos; un enum no admite un hook enter().
¿Cómo se relaciona con las máquinas de estados finitos y XState?
El patrón GoF es una forma de implementar una máquina de estados finitos con clases: estados como objetos, eventos como llamadas a métodos, transiciones como cambios de referencia. Herramientas de front-end como XState llevan el mismo concepto más lejos con statecharts (jerarquías, estados paralelos y edición visual), y el Patrón State Machine para UI cubre ese lado. Si tu pedido tiene estados anidados como shipped.in_transit y shipped.delivered, una librería de statecharts suele encajar mejor que clases a mano.
¿Cómo añado un estado nuevo sin romper los existentes?
Crea la clase, implementa los tres eventos y conecta la transición entrante desde el estado que lo produce (por ejemplo, un RefundedState alcanzable desde PaidState.cancel()). Nada más necesita recompilarse, que es justo el objetivo: con el switch habrías tenido que recorrer todos los métodos.
¿Puede un estado llamar de vuelta al contexto?
Sí — la referencia al contexto es el único canal que tiene un estado para disparar una transición. La mala señal es cuando llama más allá de transition_to y los helpers propios del estado: si PaidState busca order.payment_gateway, es que el estado conoce demasiado del interior del contexto. Pásale el contexto pero úsalo como asidero para las transiciones, no como cajón de sastre de servicios.
Recursos Relacionados
Patrón Strategy
Define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Patrón de diseño conductual para selección flexible de comportamiento.
PatternPatrón Command
Encapsula una petición como un objeto, permitiendo parametrizar clientes con colas, logs y operaciones deshacibles. Patrón de diseño conductual.
PatternPatrón Observer
Define un mecanismo de suscripción para notificar a múltiples objetos sobre eventos. Patrón de diseño conductual para comunicación basada en eventos.
PatternPatrón Mediator
Define un objeto que encapsula cómo interactúa un conjunto de objetos. Un patrón de comportamiento para reducir dependencias caóticas.
PatternPatrón Memento
Captura y restaura el estado interno de un objeto sin violar el encapsulamiento. Un patrón de comportamiento para deshacer/rehacer.