Паттерн «Стратегия» в Python
Паттерн «Стратегия» — это поведенческий паттерн проектирования, который позволяет выбирать алгоритм или поведение во время выполнения. Проще говоря, паттерн «Стратегия» инкапсулирует различные алгоритмы в независимые классы, позволяя им заменять друг друга.
Ключевая идея
Представьте, что вы пришли в ресторан заказать еду: вы можете выбрать разные способы оплаты — наличные, кредитная карта, мобильный платёж и т. д. Независимо от выбранного способа оплаты вы в конечном итоге сможете завершить оплату, но сам процесс оплаты отличается. Это пример паттерна «Стратегия» из реальной жизни.
Принципы проектирования
Паттерн «Стратегия» следует следующим важным принципам:
- Принцип открытости/закрытости: открыт для расширения, закрыт для модификации.
- Принцип единственной ответственности: каждый класс стратегии отвечает только за один алгоритм.
- Принцип инверсии зависимостей: зависеть от абстракций, а не от конкретных реализаций.
Структура паттерна «Стратегия»
Давайте разберём состав паттерна «Стратегия» с помощью UML-диаграммы классов:

Описание компонентов
Контекст
- Хранит ссылку на объект стратегии.
- Может динамически переключать стратегии.
- Делегирует работу объекту стратегии.
Стратегия (интерфейс стратегии)
- Определяет общий интерфейс для всех поддерживаемых алгоритмов.
- Объявляет метод для выполнения алгоритма.
ConcreteStrategy
- Реализует интерфейс стратегии.
- Предоставляет конкретную реализацию алгоритма.
Базовый синтаксис и реализация
Определение интерфейса стратегии
В Python мы можем использовать абстрактный базовый класс (ABC) для определения интерфейса стратегии:
Пример
class PaymentStrategy(ABC):
"""Абстрактный базовый класс стратегии оплаты"""
@abstractmethod
def pay(self, amount: float) -> bool:
"""Метод оплаты"""
pass
Реализация конкретной стратегии
Пример
"""Стратегия оплаты кредитной картой"""
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"Оплатить {amount} юаней кредитной картой")
print(f"Номер карты: {self.card_number}")
# Здесь должна быть фактическая логика оплаты
return True
class AlipayPayment(PaymentStrategy):
"""Стратегия оплаты через Alipay"""
def __init__(self, account: str):
self.account = account
def pay(self, amount: float) -> bool:
print(f"Оплатить {amount} yuan через Alipay")
print(f"Аккаунт Alipay: {self.account}")
# Здесь должна быть фактическая логика оплаты
return True
class WechatPayment(PaymentStrategy):
"""Стратегия WeChat Pay"""
def __init__(self, openid: str):
self.openid = openid
def pay(self, amount: float) -> bool:
print(f"Оплатить {amount} yuan через WeChat Pay")
print(f"WeChat OpenID: {self.openid}")
# Здесь должна быть фактическая логика оплаты
return True
Реализация класса контекста
Пример
"""Класс контекста платежа"""
def __init__(self, strategy: PaymentStrategy = None):
self._strategy = strategy
def set_strategy(self, strategy: PaymentStrategy):
"""Задать стратегию оплаты"""
self._strategy = strategy
def execute_payment(self, amount: float) -> bool:
"""Выполнить платёж"""
if not self._strategy:
raise ValueError("Стратегия оплаты не установлена")
return self._strategy.pay(amount)
Полный пример: платёжная система электронной коммерции
Давайте продемонстрируем практическое применение паттерна «Стратегия» на примере полноценной платёжной системы интернет-магазина:
Пример
from typing import Dict, Any
# Интерфейс стратегии
class DiscountStrategy(ABC):
"""Интерфейс стратегии скидок"""
@abstractmethod
def calculate_discount(self, original_price: float) -> float:
"""Рассчитать цену после скидки"""
pass
# Конкретные реализации стратегий
class NoDiscountStrategy(DiscountStrategy):
"""Стратегия без скидки"""
def calculate_discount(self, original_price: float) -> float:
return original_price
class PercentageDiscountStrategy(DiscountStrategy):
"""Стратегия процентной скидки"""
def __init__(self, percentage: float):
if not 0 <= percentage <= 100:
raise ValueError("Процент скидки должен быть в диапазоне 0-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):
"""Стратегия скидки с фиксированной суммой"""
def __init__(self, discount_amount: float):
if discount_amount < 0:
raise ValueError("Сумма скидки не может быть отрицательной")
self.discount_amount = discount_amount
def calculate_discount(self, original_price: float) -> float:
return max(0, original_price - self.discount_amount)
class SeasonalDiscountStrategy(DiscountStrategy):
"""Стратегия сезонной скидки"""
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)
# Класс контекста
class ShoppingCart:
"""Класс корзины покупок"""
def __init__(self):
self.items = []
self._discount_strategy = NoDiscountStrategy()
def add_item(self, item: str, price: float):
"""Добавить товар"""
self.items.append({"item": item, "price": price})
def set_discount_strategy(self, strategy: DiscountStrategy):
"""Установить стратегию скидки"""
self._discount_strategy = strategy
def calculate_total(self) -> float:
"""Вычислить общую стоимость"""
total = sum(item["price"] for item in self.items)
return self._discount_strategy.calculate_discount(total)
def display_cart(self):
"""Показать содержимое корзины"""
print("Содержимое корзины:")
for item in self.items:
print(f" - {item['item']}: {item['price']} yuan")
original_total = sum(item["price"] for item in self.items)
final_total = self.calculate_total()
print(f"Первоначальная цена: {original_total} yuan")
print(f"Цена со скидкой: {final_total} yuan")
if original_total != final_total:
discount = original_total - final_total
print(f"Экономия: {discount} yuan")
# Пример использования
def main():
# Создать корзину покупок
cart = ShoppingCart()
# Добавить товары
cart.add_item("Книга по программированию на Python", 89.0)
cart.add_item("Беспроводная мышь", 129.0)
cart.add_item("Механическая клавиатура", 399.0)
print("=== Без скидки ===")
cart.set_discount_strategy(NoDiscountStrategy())
cart.display_cart()
print("\n"=== Скидка 20% ===")
cart.set_discount_strategy(PercentageDiscountStrategy(20)) # Скидка 20%
cart.display_cart()
print("\n"=== Пороговая скидка (скидка 50 юаней) ===")
cart.set_discount_strategy(FixedAmountDiscountStrategy(50))
cart.display_cart()
print("\n"=== Сезонная скидка ===")
cart.set_discount_strategy(SeasonalDiscountStrategy(30, 1.5)) # Базовая скидка 30, сезонный коэффициент 1.5
cart.display_cart()
if __name__ == "__main__":
main()
Запустив приведённый выше код, вы увидите следующий вывод:
=== 无折扣 === 购物车内容: - 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元
Продвинутое использование паттерна «Стратегия»
1. Паттерн «Фабрика стратегий»
Объедините с паттерном «Фабрика», чтобы управлять созданием стратегий:
Пример
"""Фабрика стратегий скидок"""
@staticmethod
def create_strategy(strategy_type: str, **kwargs) -> DiscountStrategy:
"""Создать стратегию скидки"""
strategies = {
"no_discount": NoDiscountStrategy,
"percentage": PercentageDiscountStrategy,
"fixed_amount": FixedAmountDiscountStrategy,
"seasonal": SeasonalDiscountStrategy
}
if strategy_type not in strategies:
raise ValueError(f"Неподдерживаемый тип стратегии: {strategy_type}")
return strategies[strategy_type](**kwargs)
# Использовать паттерн фабрики
factory = DiscountStrategyFactory()
# Создать различные стратегии
strategy1 = factory.create_strategy("percentage", percentage=15) # скидка 15%
strategy2 = factory.create_strategy("fixed_amount", discount_amount=100) # скидка 100 yuan
2. Динамический выбор стратегии
Динамический выбор стратегий в зависимости от условий:
Пример
"""Динамический селектор скидок"""
@staticmethod
def select_strategy(user_type: str, total_amount: float) -> DiscountStrategy:
"""Выбор стратегии на основе типа пользователя и общей суммы"""
if user_type == "vip":
if total_amount > 500:
return PercentageDiscountStrategy(25) # VIP: скидка 25% при заказе от 500
else:
return PercentageDiscountStrategy(15) # VIP: скидка 15%
elif user_type == "normal":
if total_amount > 300:
return FixedAmountDiscountStrategy(30) # Обычные пользователи: скидка 30 юаней при заказе от 300
else:
return NoDiscountStrategy()
else:
return NoDiscountStrategy()
# Использовать динамический выбор
cart = ShoppingCart()
cart.add_item("Товар A", 200)
cart.add_item("Товар B", 150)
selector = DynamicDiscountSelector()
strategy = selector.select_strategy("vip", cart.calculate_total())
cart.set_discount_strategy(strategy)
cart.display_cart()
Преимущества и сценарии применения паттерна «Стратегия»
Сравнение преимуществ
| Характеристика | Традиционный подход | Паттерн «Стратегия» |
|---|---|---|
| Расширяемость | Требует изменения существующего кода | Достаточно просто добавить новый класс стратегии |
| Сопровождаемость | Высокая связанность кода | Разделение обязанностей, легко поддерживать |
| Гибкость | Сложно переключать алгоритмы во время выполнения | Можно динамически переключать стратегии |
| Тестируемость | Сложно тестировать алгоритмы изолированно | Каждую стратегию можно тестировать независимо |
Применимые сценарии
- Несколько вариантов алгоритма: когда у вас есть несколько похожих классов, которые отличаются только определённым поведением.
- Избегание условных операторов: когда вы хотите избежать использования большого количества условных операторов (if-else или switch).
- Выбор алгоритма во время выполнения: когда нужно выбирать разные алгоритмы во время выполнения.
- Инкапсуляция алгоритма: когда вы хотите изолировать алгоритм от клиентов, которые его используют.
Неприменимые сценарии
- Простые алгоритмы: если есть только один или два алгоритма, которые редко меняются, это может быть избыточным проектированием.
- Клиенту нужно знать детали стратегии: если клиент должен знать конкретную реализацию стратегии.
- Слишком много стратегий: когда количество классов стратегий взрывается, рассмотрите другие паттерны.
Лучшие практики и рекомендации
Рекомендации по организации кода
Пример
project/
├── strategies/
│ ├── __init__.py
│ ├── base_strategy.py # Интерфейс базовой стратегии
│ ├── discount_strategies.py # Стратегии, связанные со скидками
│ └── payment_strategies.py # Стратегии, связанные с платежами
├── contexts/
│ ├── __init__.py
│ └── shopping_cart.py # Класс контекста
└── main.py
Обработка ошибок
Пример
"""Стратегия скидок с обработкой ошибок"""
def __init__(self, base_strategy: DiscountStrategy, fallback_strategy: DiscountStrategy = Отсутствует):
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"Ошибка расчета скидки: {e}, используется резервная стратегия")
return self.fallback_strategy.calculate_discount(original_price)
Соображения производительности
Для сценариев, чувствительных к производительности, рассмотрите следующие оптимизации:
Пример
class CachedDiscountStrategy(DiscountStrategy):
"""Стратегия скидки с кэшированием"""
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)
Практические упражнения
Упражнение 1: Реализация стратегии сортировки
Реализуйте сортировщик, поддерживающий несколько алгоритмов сортировки:
Пример
from typing import List
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: List) -> List:
pass
# TODO: Реализовать стратегию пузырьковой сортировки
class BubbleSortStrategy(SortStrategy):
def sort(self, data: List) -> List:
# Ваш код реализации
pass
# TODO: Реализовать стратегию быстрой сортировки
class QuickSortStrategy(SortStrategy):
def sort(self, data: List) -> List:
# Ваш код реализации
pass
# TODO: Реализовать стратегию сортировки слиянием
class MergeSortStrategy(SortStrategy):
def sort(self, data: List) -> List:
# Ваш код реализации
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("Стратегия сортировки не установлена")
return self._strategy.sort(data)
# Протестируйте свою реализацию
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(BubbleSortStrategy())
result = sorter.sort_data(data)
print(f"Результат сортировки: {result}")
Упражнение 2: Стратегия сжатия файлов
Спроектируйте файловый компрессор, поддерживающий несколько форматов сжатия:
Пример
class CompressionStrategy(ABC):
@abstractmethod
def compress(self, file_path: str) -> str:
pass
@abstractmethod
def decompress(self, file_path: str) -> str:
pass
# TODO: Реализовать стратегию ZIP-сжатия
class ZipCompressionStrategy(CompressionStrategy):
def compress(self, file_path: str) -> str:
# Ваш код реализации
pass
def decompress(self, file_path: str) -> str:
# Ваш код реализации
pass
# TODO: Реализовать стратегию сжатия GZIP
class GzipCompressionStrategy(CompressionStrategy):
def compress(self, file_path: str) -> str:
# Ваш код реализации
pass
def decompress(self, file_path: str) -> str:
# Ваш код реализации
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("Стратегия сжатия не задана")
return self._strategy.compress(file_path)
def decompress_file(self, file_path: str) -> str:
if not self._strategy:
raise ValueError("Стратегия сжатия не задана")
return self._strategy.decompress(file_path)
Итог
Паттерн «Стратегия» — очень практичный паттерн проектирования в Python. Инкапсулируя алгоритмы в независимые классы стратегий, он обеспечивает хорошую расширяемость и гибкость. После изучения этой статьи вы должны уметь:
- Понимать основные концепции и сценарии применения паттерна «Стратегия»
- Освойте основные методы реализации паттерна «Стратегия»
- Уместно применяйте паттерн «Стратегия» в реальных проектах
- Избегайте распространённых ошибок при использовании паттерна «Стратегия»