Паттерн «Стратегия» в Python

Паттерн «Стратегия» — это поведенческий паттерн проектирования, который позволяет выбирать алгоритм или поведение во время выполнения. Проще говоря, паттерн «Стратегия» инкапсулирует различные алгоритмы в независимые классы, позволяя им заменять друг друга.

Ключевая идея

Представьте, что вы пришли в ресторан заказать еду: вы можете выбрать разные способы оплаты — наличные, кредитная карта, мобильный платёж и т. д. Независимо от выбранного способа оплаты вы в конечном итоге сможете завершить оплату, но сам процесс оплаты отличается. Это пример паттерна «Стратегия» из реальной жизни.

Принципы проектирования

Паттерн «Стратегия» следует следующим важным принципам:

  • Принцип открытости/закрытости: открыт для расширения, закрыт для модификации.
  • Принцип единственной ответственности: каждый класс стратегии отвечает только за один алгоритм.
  • Принцип инверсии зависимостей: зависеть от абстракций, а не от конкретных реализаций.

Структура паттерна «Стратегия»

Давайте разберём состав паттерна «Стратегия» с помощью UML-диаграммы классов:

Описание компонентов

Контекст

  • Хранит ссылку на объект стратегии.
  • Может динамически переключать стратегии.
  • Делегирует работу объекту стратегии.

Стратегия (интерфейс стратегии)

  • Определяет общий интерфейс для всех поддерживаемых алгоритмов.
  • Объявляет метод для выполнения алгоритма.

ConcreteStrategy

  • Реализует интерфейс стратегии.
  • Предоставляет конкретную реализацию алгоритма.

Базовый синтаксис и реализация

Определение интерфейса стратегии

В Python мы можем использовать абстрактный базовый класс (ABC) для определения интерфейса стратегии:

Пример

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    """Абстрактный базовый класс стратегии оплаты"""
   
    @abstractmethod
    def pay(self, amount: float) -> bool:
        """Метод оплаты"""
        pass

Реализация конкретной стратегии

Пример

class CreditCardPayment(PaymentStrategy):
    """Стратегия оплаты кредитной картой"""
   
    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

Реализация класса контекста

Пример

class PaymentContext:
    """Класс контекста платежа"""
   
    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 abc import ABC, abstractmethod
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. Паттерн «Фабрика стратегий»

Объедините с паттерном «Фабрика», чтобы управлять созданием стратегий:

Пример

class DiscountStrategyFactory:
    """Фабрика стратегий скидок"""
   
    @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. Динамический выбор стратегии

Динамический выбор стратегий в зависимости от условий:

Пример

class DynamicDiscountSelector:
    """Динамический селектор скидок"""
   
    @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()

Преимущества и сценарии применения паттерна «Стратегия»

Сравнение преимуществ

Характеристика Традиционный подход Паттерн «Стратегия»
Расширяемость Требует изменения существующего кода Достаточно просто добавить новый класс стратегии
Сопровождаемость Высокая связанность кода Разделение обязанностей, легко поддерживать
Гибкость Сложно переключать алгоритмы во время выполнения Можно динамически переключать стратегии
Тестируемость Сложно тестировать алгоритмы изолированно Каждую стратегию можно тестировать независимо

Применимые сценарии

  1. Несколько вариантов алгоритма: когда у вас есть несколько похожих классов, которые отличаются только определённым поведением.
  2. Избегание условных операторов: когда вы хотите избежать использования большого количества условных операторов (if-else или switch).
  3. Выбор алгоритма во время выполнения: когда нужно выбирать разные алгоритмы во время выполнения.
  4. Инкапсуляция алгоритма: когда вы хотите изолировать алгоритм от клиентов, которые его используют.

Неприменимые сценарии

  1. Простые алгоритмы: если есть только один или два алгоритма, которые редко меняются, это может быть избыточным проектированием.
  2. Клиенту нужно знать детали стратегии: если клиент должен знать конкретную реализацию стратегии.
  3. Слишком много стратегий: когда количество классов стратегий взрывается, рассмотрите другие паттерны.

Лучшие практики и рекомендации

Рекомендации по организации кода

Пример

# Рекомендуемая структура файлов
project/
├── strategies/
│   ├── __init__.py
│ ├── base_strategy.py      # Интерфейс базовой стратегии
│ ├── discount_strategies.py # Стратегии, связанные со скидками
│ └── payment_strategies.py # Стратегии, связанные с платежами
├── contexts/
│   ├── __init__.py
│ └── shopping_cart.py      # Класс контекста
└── main.py

Обработка ошибок

Пример

class SafeDiscountStrategy(DiscountStrategy):
    """Стратегия скидок с обработкой ошибок"""
   
    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)

Соображения производительности

Для сценариев, чувствительных к производительности, рассмотрите следующие оптимизации:

Пример

from functools import lru_cache

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 abc import ABC, abstractmethod
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: Стратегия сжатия файлов

Спроектируйте файловый компрессор, поддерживающий несколько форматов сжатия:

Пример

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: Реализовать стратегию 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. Инкапсулируя алгоритмы в независимые классы стратегий, он обеспечивает хорошую расширяемость и гибкость. После изучения этой статьи вы должны уметь:

  1. Понимать основные концепции и сценарии применения паттерна «Стратегия»
  2. Освойте основные методы реализации паттерна «Стратегия»
  3. Уместно применяйте паттерн «Стратегия» в реальных проектах
  4. Избегайте распространённых ошибок при использовании паттерна «Стратегия»
Другие расширения