Patrón de Estado en Python

La idea central del patrón de estado es encapsular el estado de un objeto en clases independientes y delegar el comportamiento del objeto al objeto de estado actual. Cuando el estado del objeto cambia, cambia a un objeto de estado diferente, alterando así su comportamiento.

¿Por qué necesitamos el patrón de estado?

Sin usar el patrón de estado, normalmente usamos una gran cantidad deif-elseoswitch-casesentencias para manejar comportamientos en diferentes estados. Este enfoque tiene varios problemas:

  • Código inflado: A medida que aumenta el número de estados, las sentencias condicionales se vuelven cada vez más complejas
  • Difícil de mantener: Modificar el comportamiento de un estado puede afectar a otros estados
  • Viola el principio abierto/cerrado: Agregar un nuevo estado requiere modificar el código existente

El patrón de estado resuelve estos problemas encapsulando cada estado en una clase independiente.


Estructura del patrón de estado

Roles principales

El patrón de estado incluye tres roles principales:

  1. Context (Contexto): Mantiene una instancia de un objeto de estado concreto, que define el estado actual
  2. State (Interfaz de estado): Define una interfaz para encapsular el comportamiento asociado con un estado específico del Contexto
  3. ConcreteState (Estado concreto): Implementa la interfaz State. Cada clase de estado concreto implementa un comportamiento relacionado con el estado del Contexto

Diagrama de clases UML


Implementación del patrón de estado

Pasos básicos de implementación

Entendamos la implementación del patrón de estado con un ejemplo sencillo. Supongamos que tenemos un sistema de ascensor, el ascensor tiene diferentes estados: puerta abierta, puerta cerrada, en movimiento y detenido.

Paso 1: Definir la interfaz de estado

Ejemplo

from abc import ABC, abstractmethod

class ElevatorState(ABC):
    """Interfaz de estado del ascensor"""
   
    @abstractmethod
    def open_doors(self):
        """Operación de abrir la puerta"""
        pass
   
    @abstractmethod
    def close_doors(self):
        """Operación de cerrar la puerta"""
        pass
   
    @abstractmethod
    def move(self):
        """Operación de movimiento"""
        pass
   
    @abstractmethod
    def stop(self):
        """Operación de detener"""
        pass

Paso 2: Implementar las clases de estado concreto

Ejemplo

class DoorOpenState(ElevatorState):
    """Estado de puerta abierta"""
   
    def open_doors(self):
        print("La puerta ya está abierta")
        return self
   
    def close_doors(self):
        print("Cerrando la puerta...")
        return DoorClosedState()
   
    def move(self):
        print("Error: la puerta sigue abierta, no se puede mover")
        return self
   
    def stop(self):
        print("El ascensor ya está detenido")
        return self

class DoorClosedState(ElevatorState):
    """Estado de puerta cerrada"""
   
    def open_doors(self):
        print("Abriendo la puerta...")
        return DoorOpenState()
   
    def close_doors(self):
        print("La puerta ya está cerrada")
        return self
   
    def move(self):
        print("El ascensor comienza a moverse...")
        return MovingState()
   
    def stop(self):
        print("El ascensor ya está detenido")
        return self

class MovingState(ElevatorState):
    """Estado de movimiento"""
   
    def open_doors(self):
        print("Error: el ascensor está en movimiento, no se puede abrir la puerta")
        return self
   
    def close_doors(self):
        print("La puerta ya está cerrada")
        return self
   
    def move(self):
        print("El ascensor se está moviendo")
        return self
   
    def stop(self):
        print("El ascensor se está deteniendo...")
        return DoorClosedState()

Paso 3: Crear la clase de contexto

Ejemplo

class Elevator:
    """Clase de contexto del ascensor"""
   
    def __init__(self):
        # El estado inicial es el estado de puerta cerrada
        self._state = DoorClosedState()
   
    @property
    def state(self):
        """Obtener el estado actual"""
        return self._state
   
    def set_state(self, state):
        """Establecer un nuevo estado"""
        self._state = state
        print(f"El estado ha cambiado a: {state.__class__.__name__}")
   
    def open_doors(self):
        """Operación de abrir la puerta"""
        print("Ejecutando operación de abrir la puerta...")
        self.set_state(self._state.open_doors())
   
    def close_doors(self):
        """Operación de cerrar la puerta"""
        print("Ejecutando operación de cerrar la puerta...")
        self.set_state(self._state.close_doors())
   
    def move(self):
        """Operación de movimiento"""
        print("Ejecutando operación de movimiento...")
        self.set_state(self._state.move())
   
    def stop(self):
        """Operación de detener"""
        print("Ejecutando operación de detener...")
        self.set_state(self._state.stop())

Paso 4: Usar el patrón de estado

Ejemplo

# Crear una instancia del ascensor
elevator = Elevator()

# Probar la transición de estados
print("=== Prueba de transición de estados del ascensor ===")

elevator.open_doors()   # Cambiar del estado de puerta cerrada al estado de puerta abierta
elevator.close_doors()  # Cambiar del estado de puerta abierta al estado de puerta cerrada
elevator.move()         # Cambiar del estado de puerta cerrada al estado de movimiento
elevator.stop()         # Cambiar del estado de movimiento al estado de puerta cerrada

print("\n"=== Prueba de operación ilegal ===")
elevator.open_doors()   # Abrir la puerta normalmente
elevator.move()         # Intentar moverse en el estado de puerta abierta (debería dar error)

Resultado de ejecución

=== 电梯状态转换测试 ===
执行开门操作...
正在开门...
状态已切换为: DoorOpenState
执行关门操作...
正在关门...
状态已切换为: DoorClosedState
执行移动操作...
电梯开始移动...
状态已切换为: MovingState
执行停止操作...
电梯停止中...
状态已切换为: DoorClosedState

=== 测试非法操作 ===
执行开门操作...
正在开门...
状态已切换为: DoorOpenState
执行移动操作...
错误:门还开着,不能移动
状态已切换为: DoorOpenState

Aplicación avanzada del patrón de estado

Patrón de estado con estado compartido

En algunos casos, múltiples contextos pueden necesitar compartir el mismo objeto de estado. Podemos lograr el estado compartido mediante el patrón singleton.

Ejemplo

class Singleton(type):
    """Metaclase singleton"""
    _instances = {}
   
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class SharedDoorOpenState(ElevatorState, metaclass=Singleton):
    """Estado de puerta abierta compartido (singleton)"""
   
    def open_doors(self):
        print("La puerta ya está abierta")
        return self
   
    def close_doors(self):
        print("Cerrando la puerta...")
        return SharedDoorClosedState()
   
    def move(self):
        print("Error: la puerta sigue abierta, no se puede mover")
        return self
   
    def stop(self):
        print("El ascensor ya está detenido")
        return self

class SharedDoorClosedState(ElevatorState, metaclass=Singleton):
    """Estado de puerta cerrada compartido (singleton)"""
   
    def open_doors(self):
        print("Abriendo la puerta...")
        return SharedDoorOpenState()
   
    def close_doors(self):
        print("La puerta ya está cerrada")
        return self
   
    def move(self):
        print("El ascensor comienza a moverse...")
        return SharedMovingState()
   
    def stop(self):
        print("El ascensor ya está detenido")
        return self

class SharedMovingState(ElevatorState, metaclass=Singleton):
    """Estado de movimiento compartido (singleton)"""
   
    def open_doors(self):
        print("Error: el ascensor está en movimiento, no se puede abrir la puerta")
        return self
   
    def close_doors(self):
        print("La puerta ya está cerrada")
        return self
   
    def move(self):
        print("El ascensor se está moviendo")
        return self
   
    def stop(self):
        print("El ascensor se está deteniendo...")
        return SharedDoorClosedState()

Patrón de estado y máquina de estados

El patrón de estado se utiliza a menudo junto con máquinas de estado. A continuación se muestra un ejemplo más complejo de una máquina de estados:

Ejemplo

class VendingMachineState(ABC):
    """Interfaz de estado de la máquina expendedora"""
   
    @abstractmethod
    def insert_coin(self, machine, amount):
        """Insertar moneda"""
        pass
   
    @abstractmethod
    def select_product(self, machine, product):
        """Seleccionar producto"""
        pass
   
    @abstractmethod
    def dispense(self, machine):
        """Despachar producto"""
        pass
   
    @abstractmethod
    def refund(self, machine):
        """Reembolsar"""
        pass

class WaitingState(VendingMachineState):
    """Estado de espera de moneda"""
   
    def insert_coin(self, machine, amount):
        print(f"Inserte {amount} yuan")
        machine.balance += amount
        return HasMoneyState()
   
    def select_product(self, machine, product):
        print("Por favor, inserte una moneda primero")
        return self
   
    def dispense(self, machine):
        print("Por favor, inserte una moneda y seleccione un producto primero")
        return self
   
    def refund(self, machine):
        print("No hay monto que reembolsar")
        return self

class HasMoneyState(VendingMachineState):
    """Estado de moneda insertada"""
   
    def insert_coin(self, machine, amount):
        print(f"Inserte {amount} yuan adicionales")
        machine.balance += amount
        return self
   
    def select_product(self, machine, product):
        if product.price <= machine.balance:
            print(f"Producto seleccionado: {product.name}")
            machine.selected_product = product
            return ProductSelectedState()
        else:
            print("Saldo insuficiente, por favor inserte más monedas o seleccione un producto más barato")
            return self
   
    def dispense(self, machine):
        print("Por favor, seleccione un producto primero")
        return self
   
    def refund(self, machine):
        print(f"Reembolso de {machine.balance} yuan")
        machine.balance = 0
        return WaitingState()

class ProductSelectedState(VendingMachineState):
    """Estado de producto seleccionado"""
   
    def insert_coin(self, machine, amount):
        print("Producto ya seleccionado, no se pueden insertar más monedas")
        return self
   
    def select_product(self, machine, product):
        print("Producto ya seleccionado, por favor complete la transacción actual")
        return self
   
    def dispense(self, machine):
        product = machine.selected_product
        change = machine.balance - product.price
       
        print(f"Despachando: {product.name}")
        if change > 0:
            print(f"Cambio: {change} yuan")
       
        machine.balance = 0
        machine.selected_product = None
        return WaitingState()
   
    def refund(self, machine):
        print(f"Reembolso de {machine.balance} yuan")
        machine.balance = 0
        machine.selected_product = None
        return WaitingState()

class Product:
    """Clase de producto"""
   
    def __init__(self, name, price):
        self.name = name
        self.price = price

class VendingMachine:
    """Máquina expendedora"""
   
    def __init__(self):
        self._state = WaitingState()
        self.balance = 0
        self.selected_product = None
   
    def set_state(self, state):
        self._state = state
   
    def insert_coin(self, amount):
        print(f"Operación: insertar {amount} yuan")
        self.set_state(self._state.insert_coin(self, amount))
   
    def select_product(self, product):
        print(f"Operación: seleccionar producto {product.name}")
        self.set_state(self._state.select_product(self, product))
   
    def dispense(self):
        print("Operación: solicitar despacho")
        self.set_state(self._state.dispense(self))
   
    def refund(self):
        print("Operación: solicitar reembolso")
        self.set_state(self._state.refund(self))

Ventajas y desventajas del patrón de estado

Ventajas

  1. Principio de responsabilidad única: Poner el código relacionado con un estado específico en clases independientes
  2. Principio abierto/cerrado: Introducir nuevos estados sin modificar las clases de estado existentes ni el contexto
  3. Elimina sentencias condicionales: Elimina grandes sentencias de ramas condicionales mediante llamadas polimórficas
  4. Transiciones de estado claras: Hace que las transiciones de estado sean más explícitas, reduciendo los errores debidos a transiciones de estado

Desventajas

  1. Posible sobrediseño: Si el número de estados es pequeño o estos cambian raramente, usar el patrón de estado puede ser excesivamente complejo
  2. Aumento en el número de clases de estado: Cada estado necesita una clase correspondiente, lo que puede provocar un aumento en el número de clases
  3. Contexto y acoplamiento de estadoEl contexto necesita conocer todas las clases de estado concretas para realizar las transiciones de estado

Ejercicios prácticos

Ejercicio 1: Mejorar el sistema de ascensor

Intenta añadir las siguientes funcionalidades al sistema de ascensor:

  • Añadir el estado "En mantenimiento", en el que todas las operaciones están prohibidas
  • Añadir la función de selección de planta, solo se puede seleccionar planta en el estado de parada
  • Implementar la transición automática de estado al llegar a una planta

Ejercicio 2: Implementar el sistema de semáforo

Utiliza el patrón de estado para implementar un sistema de semáforo con tres estados: luz roja, luz verde y luz amarilla. Las reglas de transición de estado son las siguientes:

  • Luz roja → Luz verde (después de 30 segundos)
  • Luz verde → Luz amarilla (después de 25 segundos)
  • Luz amarilla → Luz roja (después de 5 segundos)

Ejercicio 3: Estado del personaje de juego

Diseña un sistema de estados para un personaje de juego, que incluya los siguientes estados:

  • Estado normal: puede moverse y atacar
  • Estado envenenado: pierde vida continuamente y la velocidad de movimiento se reduce a la mitad
  • Estado aturdido: no puede moverse ni atacar
  • Estado invencible: no recibe daño

Resumen

El patrón de estado es un patrón de diseño potente, especialmente adecuado para manejar escenarios en los que el comportamiento de un objeto depende de su estado, con un número elevado de estados y transiciones complejas. Al encapsular cada estado en una clase independiente, el patrón de estado hace que el código sea más claro, fácil de mantener y de extender.

Puntos clave:

  • El patrón de estado elimina las comprobaciones condicionales al encapsular los estados como objetos
  • El contexto delega los comportamientos relacionados con el estado en el objeto de estado actual
  • Las transiciones de estado pueden ser provocadas por el contexto o por el propio objeto de estado
  • El uso adecuado del patrón de estado puede mejorar en gran medida la mantenibilidad del código
Otras extensiones