Python Patrón Estrategia

El patrón estrategia es un patrón de diseño de comportamiento que te permite seleccionar un algoritmo o comportamiento en tiempo de ejecución. En pocas palabras, el patrón estrategia consiste en encapsular diferentes algoritmos en clases independientes para que puedan intercambiarse entre sí.

Idea central

Imagina que vas a un restaurante a pagar: puedes elegir diferentes métodos de pago: efectivo, tarjeta de crédito, pago móvil, etc. No importa qué método de pago elijas, al final puedas completar el pago, solo que el proceso de pago específico es diferente. Este es un ejemplo real del patrón estrategia.

Principios de diseño

El patrón estrategia sigue los siguientes principios importantes:

  • Principio abierto/cerrado: abierto a la extensión, cerrado a la modificación
  • Principio de responsabilidad única: cada clase de estrategia se encarga de un solo algoritmo
  • Principio de inversión de dependencias: depende de abstracciones, no de implementaciones concretas

Estructura del patrón estrategia

Entendamos la estructura del patrón estrategia mediante un diagrama de clases UML:

Descripción de componentes

Context (contexto)

  • Mantiene una referencia al objeto de estrategia
  • Puede cambiar dinámicamente de estrategia
  • Delega el trabajo al objeto de estrategia

Strategy (interfaz de estrategia)

  • Define una interfaz común para todos los algoritmos admitidos
  • Declara el método de ejecución del algoritmo

ConcreteStrategy (estrategia concreta)

  • Implementa la interfaz de estrategia
  • Proporciona implementaciones de algoritmos específicos

Sintaxis básica e implementación

Definición de la interfaz de estrategia

En Python, podemos usar una clase base abstracta (ABC) para definir la interfaz de estrategia:

Ejemplo

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    """Clase base abstracta de estrategia de pago"""
   
    @abstractmethod
    def pay(self, amount: float) -> bool:
        """Método de pago"""
        pass

Implementación de estrategias concretas

Ejemplo

class CreditCardPayment(PaymentStrategy):
    """Estrategia de pago con tarjeta de crédito"""
   
    def __init__(self, card_number: str, expiry_date: str, cvv: str):
        self.card_number = card_number
        self.expiry_date = expiry_date
        self.cvv = cvv
   
    def pay(self, amount: float) -> bool:
        print(f"Pagar {amount} yuanes con tarjeta de crédito")
        print(f"Número de tarjeta: {self.card_number}")
        # Aquí debería estar la lógica de pago real
        return True

class AlipayPayment(PaymentStrategy):
    """Estrategia de pago con Alipay"""
   
    def __init__(self, account: str):
        self.account = account
   
    def pay(self, amount: float) -> bool:
        print(f"Pagar {amount} yuanes con Alipay")
        print(f"Cuenta de Alipay: {self.account}")
        # Aquí debería estar la lógica de pago real
        return True

class WechatPayment(PaymentStrategy):
    """Estrategia de pago con WeChat"""
   
    def __init__(self, openid: str):
        self.openid = openid
   
    def pay(self, amount: float) -> bool:
        print(f"Pagar {amount} yuanes con WeChat")
        print(f"OpenID de WeChat: {self.openid}")
        # Aquí debería estar la lógica de pago real
        return True

Implementación de la clase contexto

Ejemplo

class PaymentContext:
    """Clase contexto de pago"""
   
    def __init__(self, strategy: PaymentStrategy = None):
        self._strategy = strategy
   
    def set_strategy(self, strategy: PaymentStrategy):
        """Establecer estrategia de pago"""
        self._strategy = strategy
   
    def execute_payment(self, amount: float) -> bool:
        """Ejecutar pago"""
        if not self._strategy:
            raise ValueError("No se ha establecido una estrategia de pago")
       
        return self._strategy.pay(amount)

Ejemplo completo: sistema de pago de comercio electrónico

Demostremos la aplicación práctica del patrón estrategia con un sistema completo de pago de comercio electrónico:

Ejemplo

from abc import ABC, abstractmethod
from typing import Dict, Any

# Interfaz de estrategia
class DiscountStrategy(ABC):
    """Interfaz de estrategia de descuento"""
   
    @abstractmethod
    def calculate_discount(self, original_price: float) -> float:
        """Calcula el precio con descuento"""
        pass

# Implementación de estrategias concretas
class NoDiscountStrategy(DiscountStrategy):
    """Estrategia sin descuento"""
   
    def calculate_discount(self, original_price: float) -> float:
        return original_price

class PercentageDiscountStrategy(DiscountStrategy):
    """Estrategia de descuento porcentual"""
   
    def __init__(self, percentage: float):
        if not 0 <= percentage <= 100:
            raise ValueError("El porcentaje de descuento debe estar entre 0 y 100")
        self.percentage = percentage
   
    def calculate_discount(self, original_price: float) -> float:
        discount_amount = original_price * (self.percentage / 100)
        return original_price - discount_amount

class FixedAmountDiscountStrategy(DiscountStrategy):
    """Estrategia de descuento de monto fijo"""
   
    def __init__(self, discount_amount: float):
        if discount_amount < 0:
            raise ValueError("El monto del descuento no puede ser negativo")
        self.discount_amount = discount_amount
   
    def calculate_discount(self, original_price: float) -> float:
        return max(0, original_price - self.discount_amount)

class SeasonalDiscountStrategy(DiscountStrategy):
    """Estrategia de descuento estacional"""
   
    def __init__(self, base_discount: float, seasonal_multiplier: float):
        self.base_discount = base_discount
        self.seasonal_multiplier = seasonal_multiplier
   
    def calculate_discount(self, original_price: float) -> float:
        total_discount = self.base_discount * self.seasonal_multiplier
        return max(0, original_price - total_discount)

# Clase contexto
class ShoppingCart:
    """Clase carrito de compras"""
   
    def __init__(self):
        self.items = []
        self._discount_strategy = NoDiscountStrategy()
   
    def add_item(self, item: str, price: float):
        """Agregar producto"""
        self.items.append({"item": item, "price": price})
   
    def set_discount_strategy(self, strategy: DiscountStrategy):
        """Establecer estrategia de descuento"""
        self._discount_strategy = strategy
   
    def calculate_total(self) -> float:
        """Calcular precio total"""
        total = sum(item["price"] for item in self.items)
        return self._discount_strategy.calculate_discount(total)
   
    def display_cart(self):
        """Mostrar contenido del carrito"""
        print("Contenido del carrito:")
        for item in self.items:
            print(f" - {item['item']}: {item['price']} yuanes")
       
        original_total = sum(item["price"] for item in self.items)
        final_total = self.calculate_total()
       
        print(f"Precio original: {original_total} yuanes")
        print(f"Precio con descuento: {final_total} yuanes")
       
        if original_total != final_total:
            discount = original_total - final_total
            print(f"Ahorro: {discount} yuanes")

# Ejemplo de uso
def main():
    # Crear carrito de compras
    cart = ShoppingCart()
   
    # Agregar productos
    cart.add_item("Libro de programación en Python", 89.0)
    cart.add_item("Ratón inalámbrico", 129.0)
    cart.add_item("Teclado mecánico", 399.0)
   
    print("=== Sin descuento ===")
    cart.set_discount_strategy(NoDiscountStrategy())
    cart.display_cart()
   
    print("\n=== Descuento del 20% ===")
    cart.set_discount_strategy(PercentageDiscountStrategy(20))  # 20% de descuento
    cart.display_cart()
   
    print("\n=== Descuento por umbral (reducir 50 yuanes) ===)
    cart.set_discount_strategy(FixedAmountDiscountStrategy(50))
    cart.display_cart()
   
    print("\n=== Descuento estacional ===)
    cart.set_discount_strategy(SeasonalDiscountStrategy(30, 1.5))  # Descuento base 30, coeficiente estacional 1.5
    cart.display_cart()

if __name__ == "__main__":
    main()

Al ejecutar el código anterior, verás la siguiente salida:

=== 无折扣 ===
购物车内容:
  - Python编程书: 89.0元
  - 无线鼠标: 129.0元
  - 机械键盘: 399.0元
原价: 617.0元
折后价: 617.0元

=== 8折优惠 ===
购物车内容:
  - Python编程书: 89.0元
  - 无线鼠标: 129.0元
  - 机械键盘: 399.0元
原价: 617.0元
折后价: 493.6元
节省: 123.4元

=== 满减优惠(减50元)===
购物车内容:
  - Python编程书: 89.0元
  - 无线鼠标: 129.0元
  - 机械键盘: 399.0元
原价: 617.0元
折后价: 567.0元
节省: 50.0元

=== 季节性优惠 ===
购物车内容:
  - Python编程书: 89.0元
  - 无线鼠标: 129.0元
  - 机械键盘: 399.0元
原价: 617.0元
折后价: 572.0元
节省: 45.0元

Usos avanzados del patrón estrategia

1. Patrón de fábrica de estrategias

Combina el patrón de fábrica para gestionar la creación de estrategias:

Ejemplo

class DiscountStrategyFactory:
    """Fábrica de estrategias de descuento"""
   
    @staticmethod
    def create_strategy(strategy_type: str, **kwargs) -> DiscountStrategy:
        """Crea una estrategia de descuento"""
        strategies = {
            "no_discount": NoDiscountStrategy,
            "percentage": PercentageDiscountStrategy,
            "fixed_amount": FixedAmountDiscountStrategy,
            "seasonal": SeasonalDiscountStrategy
        }
       
        if strategy_type not in strategies:
            raise ValueError(f"Tipo de estrategia no soportado: {strategy_type}")
       
        return strategies[strategy_type](**kwargs)

# Usar el patrón de fábrica
factory = DiscountStrategyFactory()

# Crear diferentes estrategias
strategy1 = factory.create_strategy("percentage", percentage=15)  # 15% de descuento
strategy2 = factory.create_strategy("fixed_amount", discount_amount=100)  # Reducir 100 yuanes

2. Selección dinámica de estrategia

Selecciona dinámicamente la estrategia según las condiciones:

Ejemplo

class DynamicDiscountSelector:
    """Selector de descuento dinámico"""
   
    @staticmethod
    def select_strategy(user_type: str, total_amount: float) -> DiscountStrategy:
        """Selecciona la estrategia según el tipo de usuario y el monto total"""
        if user_type == "vip":
            if total_amount > 500:
                return PercentageDiscountStrategy(25)  # VIP con gasto de 500 obtiene 25% de descuento
            else:
                return PercentageDiscountStrategy(15)  # VIP obtiene 15% de descuento
        elif user_type == "normal":
            if total_amount > 300:
                return FixedAmountDiscountStrategy(30)  # Usuario normal con gasto de 300 obtiene 30 de descuento
            else:
                return NoDiscountStrategy()
        else:
            return NoDiscountStrategy()

# Usar selección dinámica
cart = ShoppingCart()
cart.add_item("Producto A", 200)
cart.add_item("Producto B", 150)

selector = DynamicDiscountSelector()
strategy = selector.select_strategy("vip", cart.calculate_total())
cart.set_discount_strategy(strategy)
cart.display_cart()

Ventajas y escenarios de aplicación del patrón estrategia

Comparación de ventajas

Característica Método tradicional Patrón estrategia
Extensibilidad Requiere modificar el código existente Solo necesita agregar una nueva clase de estrategia
Mantenibilidad Alto acoplamiento del código Separación de responsabilidades, fácil de mantener
Flexibilidad Difícil cambiar de algoritmo en tiempo de ejecución Puede cambiar dinámicamente de estrategia
Testabilidad Difícil probar algoritmos de forma aislada Cada estrategia se puede probar de forma independiente

Escenarios de aplicación

  1. Múltiples variantes de algoritmos: cuando tienes varias clases similares que solo difieren en ciertos comportamientos
  2. Evitar declaraciones condicionales: cuando se desea evitar el uso de muchas declaraciones condicionales (if-else o switch)
  3. Selección de algoritmo en tiempo de ejecución: cuando se necesita seleccionar diferentes algoritmos en tiempo de ejecución
  4. Encapsulamiento de algoritmos: cuando se desea aislar los detalles del algoritmo del cliente que lo usa

Escenarios no aplicables

  1. Algoritmos simples: si solo hay uno o dos algoritmos que rara vez cambian, puede ser un diseño excesivo
  2. El cliente necesita conocer los detalles de la estrategia: si el cliente debe conocer la implementación concreta de la estrategia
  3. Cantidad excesiva de estrategias: cuando el número de clases de estrategia se dispara, considera otros patrones

Mejores prácticas y precauciones

Sugerencias de organización del código

Ejemplo

# Estructura de archivos recomendada
project/
├── strategies/
│   ├── __init__.py
│   ├── base_strategy.py      # Interfaz base de estrategia
│   ├── discount_strategies.py # Estrategias relacionadas con descuentos
│   └── payment_strategies.py # Estrategias relacionadas con pagos
├── contexts/
│   ├── __init__.py
│   └── shopping_cart.py      # Clase de contexto
└── main.py

Manejo de errores

Ejemplo

class SafeDiscountStrategy(DiscountStrategy):
    """Estrategia de descuento con manejo de errores"""
   
    def __init__(self, base_strategy: DiscountStrategy, fallback_strategy: DiscountStrategy = None):
        self.base_strategy = base_strategy
        self.fallback_strategy = fallback_strategy or NoDiscountStrategy()
   
    def calculate_discount(self, original_price: float) -> float:
        try:
            return self.base_strategy.calculate_discount(original_price)
        except Exception as e:
            print(f"Error al calcular el descuento: {e}, usando estrategia de respaldo")
            return self.fallback_strategy.calculate_discount(original_price)

Consideraciones de rendimiento

Para escenarios sensibles al rendimiento, se pueden considerar las siguientes optimizaciones:

Ejemplo

from functools import lru_cache

class CachedDiscountStrategy(DiscountStrategy):
    """Estrategia de descuento con caché"""
   
    def __init__(self, base_strategy: DiscountStrategy):
        self.base_strategy = base_strategy
   
    @lru_cache(maxsize=128)
    def calculate_discount(self, original_price: float) -> float:
        return self.base_strategy.calculate_discount(original_price)

Ejercicios prácticos

Ejercicio 1: Implementación de estrategia de ordenamiento

Implementa un clasificador que admita múltiples algoritmos de ordenamiento:

Ejemplo

from abc import ABC, abstractmethod
from typing import List

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data: List) -> List:
        pass

# TODO: Implementar estrategia de ordenamiento de burbuja
class BubbleSortStrategy(SortStrategy):
    def sort(self, data: List) -> List:
        # Tu código de implementación
        pass

# TODO: Implementar estrategia de ordenamiento rápido (quicksort)
class QuickSortStrategy(SortStrategy):
    def sort(self, data: List) -> List:
        # Tu código de implementación
        pass

# TODO: Implementar estrategia de ordenamiento por fusión (mergesort)
class MergeSortStrategy(SortStrategy):
    def sort(self, data: List) -> List:
        # Tu código de implementación
        pass

class Sorter:
    def __init__(self, strategy: SortStrategy = None):
        self._strategy = strategy
   
    def set_strategy(self, strategy: SortStrategy):
        self._strategy = strategy
   
    def sort_data(self, data: List) -> List:
        if not self._strategy:
            raise ValueError("Estrategia de ordenamiento no configurada")
        return self._strategy.sort(data)

# Prueba tu implementación
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(BubbleSortStrategy())
result = sorter.sort_data(data)
print(f"Resultado del ordenamiento: {result}")

Ejercicio 2: Estrategia de compresión de archivos

Diseña un compresor de archivos que admita múltiples formatos de compresión:

Ejemplo

from abc import ABC, abstractmethod

class CompressionStrategy(ABC):
    @abstractmethod
    def compress(self, file_path: str) -> str:
        pass
   
    @abstractmethod
    def decompress(self, file_path: str) -> str:
        pass

# TODO: Implementar estrategia de compresión ZIP
class ZipCompressionStrategy(CompressionStrategy):
    def compress(self, file_path: str) -> str:
        # Tu código de implementación
        pass
   
    def decompress(self, file_path: str) -> str:
        # Tu código de implementación
        pass

# TODO: Implementar estrategia de compresión GZIP
class GzipCompressionStrategy(CompressionStrategy):
    def compress(self, file_path: str) -> str:
        # Tu código de implementación
        pass
   
    def decompress(self, file_path: str) -> str:
        # Tu código de implementación
        pass

class FileCompressor:
    def __init__(self, strategy: CompressionStrategy = None):
        self._strategy = strategy
   
    def set_strategy(self, strategy: CompressionStrategy):
        self._strategy = strategy
   
    def compress_file(self, file_path: str) -> str:
        if not self._strategy:
            raise ValueError("Estrategia de compresión no configurada")
        return self._strategy.compress(file_path)
   
    def decompress_file(self, file_path: str) -> str:
        if not self._strategy:
            raise ValueError("Estrategia de compresión no configurada")
        return self._strategy.decompress(file_path)

Resumen

El patrón de estrategia es un patrón de diseño muy práctico en Python. Al encapsular algoritmos en clases de estrategia independientes, proporciona buena extensibilidad y flexibilidad. Después de estudiar este artículo, deberías ser capaz de:

  1. Comprender los conceptos centrales del patrón de estrategia y sus escenarios de aplicación
  2. Dominar los métodos básicos de implementación del patrón de estrategia
  3. Aplicar el patrón de estrategia adecuadamente en proyectos reales
  4. Evitar los usos incorrectos comunes del patrón de estrategia
Otras extensiones