Patrones de Diseño: El Patrón State (GoF)
El patrón State (Estado) es uno de los patrones comportamentales descritos en Design Patterns: Elements of Reusable Object-Oriented Software (GoF, 1994). Permite que un objeto altere su comportamiento cuando cambia su estado interno, haciendo que parezca que el objeto ha cambiado de clase.
¿Qué es el patrón State?
State encapsula el comportamiento asociado a cada estado en clases separadas, delegando
las operaciones del contexto a la instancia del estado actual. Es una alternativa a grandes estructuras
condicionales (if-else o switch) que se vuelven inmantenibles cuando el número de estados crece.
Está relacionado con las máquinas de estados finitas (FSM), pero aplicado de forma orientada a objetos, facilitando extensibilidad y el cumplimiento de principios SOLID (especialmente OCP y SRP).
¿Cuándo usar el patrón State?
Aplícalo cuando:
- El comportamiento depende fuertemente del estado interno y cambia de forma significativa entre estados.
- Tenés código con condicionales por todos lados verificando estado.
- Necesitás agregar estados frecuentemente sin modificar el código existente del Context.
- Querés modelar claramente transiciones y acciones asociadas (entry, exit, do).
Ejemplos clásicos: reproductores multimedia (Playing/Paused/Stopped), conexiones TCP (Established/Listening/Closed), pedidos e-commerce (Nuevo/Procesando/Enviado/Entregado/Cancelado), o estados de un personaje en un videojuego.
Estructura del patrón State (según el GoF)
El patrón State está compuesto por tres elementos principales:
Context
- state: State
+ request()
<<interface>> State
+ handle()
ConcreteStateA
+ handle() (cambia context.state)
ConcreteStateB
+ handle() (cambia context.state)
Componentes principales
- Context: mantiene una referencia al estado actual y delega solicitudes.
- State: interfaz/clase abstracta que define la API de estados.
- ConcreteState: implementa comportamiento específico y gestiona transiciones.
Ventajas y desventajas
Ventajas
- Open-Closed: agregás estados creando nuevas clases sin tocar el Context.
- Single Responsibility: cada estado tiene su lógica delimitada.
- Eliminación de condicionales: evita switch/if-else gigantes.
- Claridad: el modelo refleja el diagrama de estados del dominio.
- Testing: podés probar cada estado de forma aislada.
Desventajas y consideraciones
- Aumenta el número de clases (una por estado).
- Overhead leve en memoria/tiempo de ejecución.
- Con pocos estados y transiciones simples, puede ser sobreingeniería (enum + switch puede alcanzar).
Ejemplo práctico en Python
Implementación de un reproductor con tres estados: Stopped, Playing y Paused.
from abc import ABC, abstractmethod
# Interfaz State
class PlayerState(ABC):
@abstractmethod
def play(self, player): ...
@abstractmethod
def pause(self, player): ...
@abstractmethod
def stop(self, player): ...
# Estados Concretos
class StoppedState(PlayerState):
def play(self, player):
print("Iniciando reproducción...")
player.set_state(PlayingState())
def pause(self, player):
print("No se puede pausar: ya está detenido.")
def stop(self, player):
print("Ya está detenido.")
class PlayingState(PlayerState):
def play(self, player):
print("Ya está reproduciendo.")
def pause(self, player):
print("Pausando reproducción...")
player.set_state(PausedState())
def stop(self, player):
print("Deteniendo reproducción...")
player.set_state(StoppedState())
class PausedState(PlayerState):
def play(self, player):
print("Reanudando reproducción...")
player.set_state(PlayingState())
def pause(self, player):
print("Ya está pausado.")
def stop(self, player):
print("Deteniendo desde pausado...")
player.set_state(StoppedState())
# Context
class MediaPlayer:
def __init__(self):
self._state = StoppedState() # Estado inicial
def set_state(self, state: PlayerState):
self._state = state
def play(self):
self._state.play(self)
def pause(self):
self._state.pause(self)
def stop(self):
self._state.stop(self)
# Uso
player = MediaPlayer()
player.play() # Iniciando reproducción...
player.pause() # Pausando reproducción...
player.play() # Reanudando reproducción...
player.stop() # Deteniendo reproducción...
player.pause() # No se puede pausar: ya está detenido.
State vs alternativas
Regla práctica
Si tu objeto vive en un if/elif/else infinito y cada feature le suma otra rama,
State no es sobreingeniería: es control de daños.
State vs Strategy
- Strategy cambia cómo se ejecuta un algoritmo (mismo objetivo, distintas implementaciones).
- State cambia qué está permitido hacer y el comportamiento global según el estado.
- En State suele haber transiciones (una acción puede cambiar el estado).
State vs enum + switch
- enum + switch arranca rápido, pero escala mal cuando crecen estados y reglas.
- State reparte la complejidad: cada estado encapsula comportamiento + validaciones + transiciones.
- Con 2–3 estados simples que casi no cambian, enum+switch puede ser suficiente.
Checklist para decidir
- ¿Reglas distintas por estado? → sí
- ¿Transiciones con validaciones (“solo si…”) ? → sí
- ¿Se agregan estados con frecuencia? → sí
- ¿Tu lógica hoy es un condicional gigante? → sí
- Si marcaste 2 o más: State suele pagar.
Transiciones y acciones on_enter / on_exit
En sistemas reales no alcanza con “cambiar el comportamiento”. Necesitás controlar transiciones y ejecutar acciones al entrar/salir (logs, métricas, eventos, auditoría).
from abc import ABC
class PlayerState(ABC):
def on_enter(self, player): ...
def on_exit(self, player): ...
class PlayingState(PlayerState):
def on_enter(self, player):
player.metrics["starts"] += 1
class MediaPlayer:
def __init__(self):
self.metrics = {"starts": 0}
self._state = None
def set_state(self, state: PlayerState):
# exit del estado actual
if self._state and hasattr(self._state, "on_exit"):
self._state.on_exit(self)
self._state = state
# enter del nuevo estado
if hasattr(self._state, "on_enter"):
self._state.on_enter(self)
Ejemplo realista: ciclo de vida de un pedido (e-commerce)
Un pedido típico: Nuevo → Pagado → Enviado → Entregado, con salida como Cancelado. Cada estado define qué acciones están permitidas.
from abc import ABC, abstractmethod
class OrderState(ABC):
@abstractmethod
def pay(self, order): ...
@abstractmethod
def ship(self, order): ...
@abstractmethod
def deliver(self, order): ...
@abstractmethod
def cancel(self, order): ...
class New(OrderState):
def pay(self, order):
order.set_state(Paid())
def ship(self, order):
raise ValueError("No podés enviar: falta pago.")
def deliver(self, order):
raise ValueError("No podés entregar: no fue enviado.")
def cancel(self, order):
order.set_state(Canceled())
class Paid(OrderState):
def pay(self, order):
raise ValueError("Ya está pagado.")
def ship(self, order):
order.set_state(Shipped())
def deliver(self, order):
raise ValueError("No podés entregar: no fue enviado.")
def cancel(self, order):
order.set_state(Canceled())
class Shipped(OrderState):
def pay(self, order): raise ValueError("No aplica.")
def ship(self, order): raise ValueError("Ya está enviado.")
def deliver(self, order):
order.set_state(Delivered())
def cancel(self, order):
raise ValueError("No podés cancelar: ya fue enviado.")
class Delivered(OrderState):
def pay(self, order): raise ValueError("No aplica.")
def ship(self, order): raise ValueError("No aplica.")
def deliver(self, order): raise ValueError("Ya entregado.")
def cancel(self, order): raise ValueError("No podés cancelar: ya entregado.")
class Canceled(OrderState):
def pay(self, order): raise ValueError("Pedido cancelado.")
def ship(self, order): raise ValueError("Pedido cancelado.")
def deliver(self, order): raise ValueError("Pedido cancelado.")
def cancel(self, order): raise ValueError("Ya cancelado.")
class Order:
def __init__(self):
self._state = New()
self.history = ["New"]
def set_state(self, state: OrderState):
self._state = state
self.history.append(state.__class__.__name__)
def pay(self): self._state.pay(self)
def ship(self): self._state.ship(self)
def deliver(self): self._state.deliver(self)
def cancel(self): self._state.cancel(self)
o = Order()
o.pay()
o.ship()
o.deliver()
print(o.history)
Tip de persistencia (DB)
Guardá el estado como string/enum (ej: "Paid") y resolvelo con una fábrica al cargar.
Evitás serializar objetos y simplificás auditoría y migraciones.
Variantes profesionales del patrón
Estados compartidos (Singleton/Flyweight)
Si los estados son “puros” (sin datos por instancia), podés instanciarlos una vez y reutilizarlos.
Transiciones centralizadas vs distribuidas
- Distribuidas: cada
ConcreteStatedecide a qué estado pasar (más OO). - Centralizadas: el Context mantiene una tabla de transiciones (útil si el grafo es grande).
Eventos en vez de métodos
En dominios complejos, podés usar dispatch(event) en lugar de play()/pause().
Mejora logging, métricas y pruebas.
Errores comunes
- El Context vuelve a tener if/else por estado: se “deshace” el patrón.
- Interfaz State gigante: dejá solo métodos que cambian por estado.
- Transiciones sin trazabilidad: guardá historial o logs.
Testing rápido por estado
import pytest
def test_new_cannot_ship():
o = Order()
with pytest.raises(ValueError):
o.ship()
def test_paid_can_ship():
o = Order()
o.pay()
o.ship()
assert o.history[-1] == "Shipped"
Relación con máquinas de estados jerárquicas (HSM)
El patrón State es una base natural para HSM: los estados pueden tener subestados,
o podés implementar un “superestado” por composición. En un reproductor podrías extender PlayingState
con subestados como “Buffering” o “Streaming”.
- Estados con referencias a subestados.
- Composición para superestados.
- Frameworks: XState (JS), Stateless (C#), Spring StateMachine (Java), etc.
Mejores prácticas profesionales
- Interfaz State con métodos que realmente varían por estado.
- Transiciones centralizadas en ConcreteState (o tabla aparte si crece mucho).
- Acciones on_enter/on_exit si necesitás hooks.
- Combinación con Strategy (algoritmos dentro de un estado) o Flyweight (muchos estados similares).
Conclusión
El patrón State modela comportamiento dependiente del estado de forma limpia y extensible. Convierte condicionales frágiles en una estructura orientada a objetos que escala mejor cuando crecen estados y transiciones.
Patrones relacionados
Resumen en 60 segundos
- State encapsula comportamiento por estado y reduce condicionales.
- Brilla cuando crecen estados, transiciones y reglas.
- Si son pocos estados y simples: enum+switch puede bastar.