← ← Volver a todas las notas

Patrón State: el antídoto contra los if/else interminables

2025-12-15 · Benja

Descubrí cómo el patrón State te permite modelar comportamientos que cambian según el estado sin llenar tu código de condicionales. Con ejemplos prácticos en Python y un caso real de e-commerce, aprendé a diseñar sistemas más mantenibles, extensibles y fáciles de testear.

Patrón State: el antídoto contra los if/else interminables

Patrones de Diseño: El Patrón State (GoF)

Publicado: · Tiempo de lectura: 10–12 min

Patrones de Diseño Gang of Four State Pattern

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)
Ojo: si vas a compartir instancias de estados (singleton/flyweight), no guardes datos mutables adentro del estado.

Ejemplo realista: ciclo de vida de un pedido (e-commerce)

Un pedido típico: NuevoPagadoEnviadoEntregado, 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 ConcreteState decide 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

Strategy Observer Command Flyweight Memento

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.

Comentarios

0 comentarios

Dejá tu comentario

Se publicará cuando sea aprobado.

Todavía no hay comentarios.