Patron de stratégie Python

Le modèle de stratégie est un patron de conception comportemental qui vous permet de choisir un algorithme ou un comportement à l’exécution. En termes simples, le modèle de stratégie encapsule différents algorithmes dans des classes indépendantes, leur permettant de se remplacer mutuellement.

Idée centrale

Imaginez que vous allez au restaurant pour commander à manger : vous pouvez choisir différents modes de paiement - espèces, carte de crédit, paiement mobile, etc. Quel que soit le mode de paiement que vous choisissez, vous pouvez finalement effectuer le paiement, mais le processus de paiement spécifique est différent. C’est un exemple concret du modèle de stratégie.

Principes de conception

Le patron de conception Stratégie suit les principes importants suivants :

  • Principe ouvert/fermé: ouvert à l'extension, fermé à la modification.
  • Principe de responsabilité unique: chaque classe de stratégie n'est responsable que d'un seul algorithme.
  • Principe d'inversion des dépendances: dépendez d'abstractions, pas d'implémentations concrètes.

Structure du patron de conception Stratégie

Comprenons la composition du patron de conception Stratégie à l'aide d'un diagramme de classes UML :

Description des composants

Contexte

  • Maintient une référence à un objet stratégie.
  • Peut changer dynamiquement de stratégie.
  • Délègue le travail à l'objet stratégie.

Stratégie (interface Strategy)

  • Définit l'interface commune pour tous les algorithmes pris en charge.
  • Déclare la méthode d'exécution de l'algorithme.

ConcreteStrategy

  • Implémente l'interface de stratégie.
  • Fournit une implémentation concrète de l'algorithme.

Syntaxe de base et implémentation

Définition de l'interface de stratégie

En Python, nous pouvons utiliser une classe de base abstraite (ABC) pour définir l'interface de stratégie :

Exemple

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    """Classe de base abstraite de la stratégie de paiement"""
   
    @abstractmethod
    def pay(self, amount: float) -> bool:
        """Méthode de paiement"""
        pass

Stratégie concrète

Exemple

class CreditCardPayment(PaymentStrategy):
    """Stratégie de paiement par carte de crédit"""
   
    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"Payer {amount} yuan par carte de crédit")
        print(f"Numéro de carte : {self.card_number}")
        # La logique de paiement réelle doit être ici
        return True

class AlipayPayment(PaymentStrategy):
    """Stratégie de paiement Alipay"""
   
    def __init__(self, account: str):
        self.account = account
   
    def pay(self, amount: float) -> bool:
        print(f"Payer {amount} yuan avec Alipay")
        print(f"Compte Alipay : {self.account}")
        # La logique de paiement réelle doit être ici
        return True

class WechatPayment(PaymentStrategy):
    """Stratégie de paiement WeChat Pay"""
   
    def __init__(self, openid: str):
        self.openid = openid
   
    def pay(self, amount: float) -> bool:
        print(f"Payer {amount} yuan avec WeChat Pay")
        print(f"OpenID WeChat : {self.openid}")
        # La logique de paiement réelle doit être ici
        return True

Implémentation de la classe de contexte

Exemple

class PaymentContext:
    """Classe de contexte de paiement"""
   
    def __init__(self, strategy: PaymentStrategy = None):
        self._strategy = strategy
   
    def set_strategy(self, strategy: PaymentStrategy):
        """Définir la stratégie de paiement"""
        self._strategy = strategy
   
    def execute_payment(self, amount: float) -> bool:
        """Exécuter le paiement"""
        if not self._strategy:
            raise ValueError("Aucune stratégie de paiement définie")
       
        return self._strategy.pay(amount)

Exemple complet : Système de paiement e-commerce

Démontrons l'application pratique du patron de stratégie à travers un système de paiement e-commerce complet :

Exemple

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

# Interface de stratégie
class DiscountStrategy(ABC):
    """Interface de stratégie de réduction"""
   
    @abstractmethod
    def calculate_discount(self, original_price: float) -> float:
        """Calculer le prix après réduction"""
        pass

# Implémentations concrètes des stratégies
class NoDiscountStrategy(DiscountStrategy):
    """Stratégie sans réduction"""
   
    def calculate_discount(self, original_price: float) -> float:
        return original_price

class PercentageDiscountStrategy(DiscountStrategy):
    """Stratégie de réduction en pourcentage"""
   
    def __init__(self, percentage: float):
        if not 0 <= percentage <= 100:
            raise ValueError("Le pourcentage de remise doit être compris entre 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):
    """Stratégie de remise à montant fixe"""
   
    def __init__(self, discount_amount: float):
        if discount_amount < 0:
            raise ValueError("Le montant de la remise ne peut pas être négatif")
        self.discount_amount = discount_amount
   
    def calculate_discount(self, original_price: float) -> float:
        return max(0, original_price - self.discount_amount)

class SeasonalDiscountStrategy(DiscountStrategy):
    """Stratégie de remise saisonnière"""
   
    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)

# Classe de contexte
class ShoppingCart:
    """Classe de panier d'achat"""
   
    def __init__(self):
        self.items = []
        self._discount_strategy = NoDiscountStrategy()
   
    def add_item(self, item: str, price: float):
        """Ajouter un article"""
        self.items.append({"item": item, "price": price})
   
    def set_discount_strategy(self, strategy: DiscountStrategy):
        """Définir la stratégie de remise"""
        self._discount_strategy = strategy
   
    def calculate_total(self) -> float:
        """Calculer le prix total"""
        total = sum(item["price"] for item in self.items)
        return self._discount_strategy.calculate_discount(total)
   
    def display_cart(self):
        """Afficher le contenu du panier"""
        print("Contenu du panier :")
        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"Prix d'origine : {original_total} yuan")
        print(f"Prix réduit : {final_total} yuan")
       
        if original_total != final_total:
            discount = original_total - final_total
            print(f"Économies : {discount} yuan")

# Exemple d'utilisation
def main():
    # Créer un panier d'achat
    cart = ShoppingCart()
   
    # Ajouter des articles
    cart.add_item("Livre de programmation Python", 89.0)
    cart.add_item("Souris sans fil", 129.0)
    cart.add_item("Clavier mécanique", 399.0)
   
    print("=== Aucune remise ===")
    cart.set_discount_strategy(NoDiscountStrategy())
    cart.display_cart()
   
    print("\n"=== 20% de réduction ===")
    cart.set_discount_strategy(PercentageDiscountStrategy(20))  # 20% de réduction
    cart.display_cart()
   
    print("\n"=== Remise sur seuil (50 yuan de réduction) ===")
    cart.set_discount_strategy(FixedAmountDiscountStrategy(50))
    cart.display_cart()
   
    print("\n"=== Remise saisonnière ===")
    cart.set_discount_strategy(SeasonalDiscountStrategy(30, 1.5))  # Remise de base 30, coefficient saisonnier 1.5
    cart.display_cart()

if __name__ == "__main__":
    main()

En exécutant le code ci-dessus, vous verrez la sortie suivante :

=== 无折扣 ===
购物车内容:
  - 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元

Utilisation avancée du patron de stratégie

1. Patron de fabrique de stratégies

Combinez le patron de fabrique pour gérer la création de stratégies :

Exemple

class DiscountStrategyFactory:
    """Fabrique de stratégies de remise"""
   
    @staticmethod
    def create_strategy(strategy_type: str, **kwargs) -> DiscountStrategy:
        """Créer une stratégie de remise"""
        strategies = {
            "no_discount": NoDiscountStrategy,
            "percentage": PercentageDiscountStrategy,
            "fixed_amount": FixedAmountDiscountStrategy,
            "seasonal": SeasonalDiscountStrategy
        }
       
        if strategy_type not in strategies:
            raise ValueError(f"Type de stratégie non pris en charge : {strategy_type}")
       
        return strategies[strategy_type](**kwargs)

# Utiliser le patron de fabrique
factory = DiscountStrategyFactory()

# Créer différentes stratégies
strategy1 = factory.create_strategy("percentage", percentage=15)  # 15% de réduction
strategy2 = factory.create_strategy("fixed_amount", discount_amount=100)  # 100 yuan de réduction

2. Sélection dynamique des stratégies

Sélectionner dynamiquement les stratégies en fonction des conditions :

Exemple

class DynamicDiscountSelector:
    """Sélecteur dynamique de remise"""
   
    @staticmethod
    def select_strategy(user_type: str, total_amount: float) -> DiscountStrategy:
        """Sélectionner la stratégie en fonction du type d'utilisateur et du montant total"""
        if user_type == "vip":
            if total_amount > 500:
                return PercentageDiscountStrategy(25)  # VIP : 25% de réduction pour les commandes de 500 ou plus
            else:
                return PercentageDiscountStrategy(15)  # VIP : 15% de réduction
        elif user_type == "normal":
            if total_amount > 300:
                return FixedAmountDiscountStrategy(30)  # Utilisateurs réguliers : 30 yuan de réduction pour les commandes de 300 ou plus
            else:
                return NoDiscountStrategy()
        else:
            return NoDiscountStrategy()

# Utiliser la sélection dynamique
cart = ShoppingCart()
cart.add_item("Product A", 200)
cart.add_item("Product B", 150)

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

Avantages et scénarios d'application du patron Stratégie

Comparaison des avantages

Fonctionnalité Approche traditionnelle Patron Stratégie
Extensibilité Nécessite de modifier le code existant Ajouter simplement une nouvelle classe de stratégie
Kemudahan pemeliharaan Fort couplage du code Séparation des responsabilités, facile à maintenir
Flexibilité Difficile de changer d'algorithme à l'exécution Permet de changer dynamiquement de stratégie
Testabilité Difficile de tester les algorithmes de manière isolée Chaque stratégie peut être testée indépendamment

Scénarios applicables

  1. Variantes multiples d'algorithmes: Lorsque vous avez plusieurs classes similaires qui ne diffèrent que par certains comportements.
  2. Éviter les instructions conditionnelles: Lorsque vous voulez éviter d'utiliser un grand nombre d'instructions conditionnelles (if-else ou switch).
  3. Sélection d'algorithme à l'exécution: Lorsque vous devez choisir différents algorithmes au moment de l'exécution.
  4. Encapsulation d'algorithme: Lorsque vous souhaitez isoler les détails de l'algorithme des clients qui utilisent l'algorithme.

Scénarios inapplicables

  1. Algorithmes simples: S'il n'y a qu'un ou deux algorithmes qui changent rarement, cela peut être de la sur-ingénierie.
  2. Le client doit connaître les détails de la stratégie: Si le client doit connaître l'implémentation concrète de la stratégie.
  3. Trop de stratégies: Lorsque le nombre de classes de stratégie explose, envisagez d'autres patrons.

Bonnes pratiques et considérations

Suggestions d'organisation du code

Exemple

# Structure de fichiers recommandée
project/
├── strategies/
│   ├── __init__.py
│ ├── base_strategy.py      # Interface de stratégie de base
│ ├── discount_strategies.py # Stratégies liées aux remises
│ └── payment_strategies.py # Stratégies liées au paiement
├── contexts/
│   ├── __init__.py
│ └── shopping_cart.py      # Classe de contexte
└── main.py

Gestion des erreurs

Exemple

class SafeDiscountStrategy(DiscountStrategy):
    """Stratégie de remise avec gestion des erreurs"""
   
    def __init__(self, base_strategy: DiscountStrategy, fallback_strategy: DiscountStrategy = Aucune):
        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"Erreur de calcul de remise : {e}, utilisation de la stratégie de secours")
            return self.fallback_strategy.calculate_discount(original_price)

Considérations de performance

Pour les scénarios sensibles aux performances, envisagez les optimisations suivantes :

Exemple

from functools import lru_cache

class CachedDiscountStrategy(DiscountStrategy):
    """Stratégie de remise avec mise en cache"""
   
    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)

Exercices pratiques

Exercice 1 : Implémentation de la stratégie de tri

Implémentez un trieur qui prend en charge plusieurs algorithmes de tri :

Exemple

from abc import ABC, abstractmethod
from typing import List

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

# TODO: Implémenter la stratégie de tri à bulles
class BubbleSortStrategy(SortStrategy):
    def sort(self, data: List) -> List:
        # Votre code d'implémentation
        pass

# TODO: Implémenter la stratégie de tri rapide
class QuickSortStrategy(SortStrategy):
    def sort(self, data: List) -> List:
        # Votre code d'implémentation
        pass

# TODO: Implémenter la stratégie de tri fusion
class MergeSortStrategy(SortStrategy):
    def sort(self, data: List) -> List:
        # Votre code d'implémentation
        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("Stratégie de tri non définie")
        return self._strategy.sort(data)

# Testez votre implémentation
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(BubbleSortStrategy())
result = sorter.sort_data(data)
print(f"Résultat du tri : {result}")

Exercice 2 : Stratégie de compression de fichiers

Concevez un compresseur de fichiers qui prend en charge plusieurs formats de compression :

Exemple

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: Implémenter la stratégie de compression ZIP
class ZipCompressionStrategy(CompressionStrategy):
    def compress(self, file_path: str) -> str:
        # Votre code d'implémentation
        pass
   
    def decompress(self, file_path: str) -> str:
        # Votre code d'implémentation
        pass

# TODO: Implémenter la stratégie de compression GZIP
class GzipCompressionStrategy(CompressionStrategy):
    def compress(self, file_path: str) -> str:
        # Votre code d'implémentation
        pass
   
    def decompress(self, file_path: str) -> str:
        # Votre code d'implémentation
        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("Stratégie de compression non définie")
        return self._strategy.compress(file_path)
   
    def decompress_file(self, file_path: str) -> str:
        if not self._strategy:
            raise ValueError("Stratégie de compression non définie")
        return self._strategy.decompress(file_path)

Résumé

Le patron de conception Strategy est un patron de conception très pratique en Python. En encapsulant les algorithmes dans des classes de stratégie indépendantes, il offre une bonne extensibilité et flexibilité. Après avoir étudié cet article, vous devriez être capable de :

  1. Comprendre les concepts fondamentaux et les scénarios d'application du patron de conception Strategy
  2. Maîtrisez les méthodes d'implémentation de base du patron Stratégie
  3. Appliquez le patron Stratégie de manière appropriée dans des projets réels
  4. Évitez les utilisations abusives courantes du patron Stratégie
Autres extensions