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
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
"""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
"""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 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
"""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
"""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
- Múltiples variantes de algoritmos: cuando tienes varias clases similares que solo difieren en ciertos comportamientos
- Evitar declaraciones condicionales: cuando se desea evitar el uso de muchas declaraciones condicionales (if-else o switch)
- Selección de algoritmo en tiempo de ejecución: cuando se necesita seleccionar diferentes algoritmos en tiempo de ejecución
- Encapsulamiento de algoritmos: cuando se desea aislar los detalles del algoritmo del cliente que lo usa
Escenarios no aplicables
- Algoritmos simples: si solo hay uno o dos algoritmos que rara vez cambian, puede ser un diseño excesivo
- El cliente necesita conocer los detalles de la estrategia: si el cliente debe conocer la implementación concreta de la estrategia
- 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
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
"""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
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 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
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:
- Comprender los conceptos centrales del patrón de estrategia y sus escenarios de aplicación
- Dominar los métodos básicos de implementación del patrón de estrategia
- Aplicar el patrón de estrategia adecuadamente en proyectos reales
- Evitar los usos incorrectos comunes del patrón de estrategia