Patron d'état Python

L'idée centrale du patron État est d'encapsuler l'état d'un objet dans des classes indépendantes et de déléguer le comportement de l'objet à l'objet d'état courant. Lorsque l'état de l'objet change, il passe à un autre objet d'état, modifiant ainsi son comportement.

Pourquoi avons-nous besoin du patron État ?

Sans utiliser le patron État, nous utilisons généralement un grand nombre deif-elseouswitch-cased'instructions pour gérer le comportement dans différents états. Cette approche présente plusieurs problèmes :

  • Gonflement du code: À mesure que le nombre d'états augmente, les instructions conditionnelles deviennent de plus en plus complexes
  • Difficile à maintenir: Modifier le comportement d'un état peut affecter d'autres états
  • Viole le principe ouvert-fermé: L'ajout d'un nouvel état nécessite de modifier le code existant

Le patron État résout ces problèmes en encapsulant chaque état dans une classe indépendante.


Structure du patron État

Rôles principaux

Le patron État se compose de trois rôles principaux :

  1. Context (contexte): maintient une instance d'un objet d'état concret ; cette instance définit l'état actuel
  2. State (interface d'état): définit une interface pour encapsuler le comportement associé à un état particulier du Context
  3. ConcreteState (état concret): implémente l'interface State ; chaque classe d'état concrète implémente un comportement lié à un état du Context

Diagramme de classes UML


Implémentation du patron State

Étapes d'implémentation de base

Comprenons la mise en œuvre du patron State à travers un exemple simple. Supposons que nous avons un système d'ascenseur où l'ascenseur possède différents états : ouvert, fermé, en mouvement et arrêté.

Étape 1 : Définir l'interface State

Exemple

from abc import ABC, abstractmethod

class ElevatorState(ABC):
    """État de l'ascenseur"""
   
    @abstractmethod
    def open_doors(self):
        """Opération d'ouverture des portes"""
        pass
   
    @abstractmethod
    def close_doors(self):
        """Opération de fermeture de porte"""
        pass
   
    @abstractmethod
    def move(self):
        """Opération de déplacement"""
        pass
   
    @abstractmethod
    def stop(self):
        """Opération d'arrêt"""
        pass

Étape 2 : Implémenter les classes d'état concrètes

Exemple

class DoorOpenState(ElevatorState):
    """État ouvert"""
   
    def open_doors(self):
        print("La porte est déjà ouverte")
        return self
   
    def close_doors(self):
        print("Fermeture de la porte...")
        return DoorClosedState()
   
    def move(self):
        print("Erreur : La porte est encore ouverte, impossible de se déplacer")
        return self
   
    def stop(self):
        print("L'ascenseur s'est arrêté")
        return self

class DoorClosedState(ElevatorState):
    """État fermé"""
   
    def open_doors(self):
        print("Ouverture de la porte...")
        return DoorOpenState()
   
    def close_doors(self):
        print("La porte est déjà fermée")
        return self
   
    def move(self):
        print("L'ascenseur commence à se déplacer...")
        return MovingState()
   
    def stop(self):
        print("L'ascenseur s'est arrêté")
        return self

class MovingState(ElevatorState):
    """État en mouvement"""
   
    def open_doors(self):
        print("Erreur : L'ascenseur est en mouvement, impossible d'ouvrir la porte")
        return self
   
    def close_doors(self):
        print("La porte est déjà fermée")
        return self
   
    def move(self):
        print("L'ascenseur est en mouvement")
        return self
   
    def stop(self):
        print("L'ascenseur s'arrête...")
        return DoorClosedState()

Étape 3 : Créer la classe de contexte

Exemple

class Elevator:
    """Classe de contexte Elevator"""
   
    def __init__(self):
        # L'état initial est l'état de porte fermée
        self._state = DoorClosedState()
   
    @property
    def state(self):
        """Obtenir l'état actuel"""
        return self._state
   
    def set_state(self, state):
        """Définir le nouvel état"""
        self._state = state
        print(f"État changé en : {state.__class__.__name__}")
   
    def open_doors(self):
        """Opération d'ouverture des portes"""
        print("Exécution de l'opération d'ouverture de porte...")
        self.set_state(self._state.open_doors())
   
    def close_doors(self):
        """Opération de fermeture de porte"""
        print("Exécution de l'opération de fermeture de porte...")
        self.set_state(self._state.close_doors())
   
    def move(self):
        """Opération de déplacement"""
        print("Exécution de l'opération de déplacement...")
        self.set_state(self._state.move())
   
    def stop(self):
        """Opération d'arrêt"""
        print("Exécution de l'opération d'arrêt...")
        self.set_state(self._state.stop())

Étape 4 : Utiliser le patron État

Exemple

# Créer une instance d'ascenseur
elevator = Elevator()

# Test des transitions d'état
print("=== Test de transition d'état de l'ascenseur ===")

elevator.open_doors()   # Passer de l'état fermé à l'état ouvert
elevator.close_doors()  # Revenir de l'état ouvert à l'état fermé
elevator.move()         # Passer de l'état fermé à l'état en mouvement
elevator.stop()         # Revenir de l'état en mouvement à l'état fermé

print("\n=== Test d'opération interdite ===)
elevator.open_doors()   # Ouvrir la porte normalement
elevator.move()         # Essayer de se déplacer pendant que la porte est ouverte (doit déclencher une erreur)

Résultat de l'exécution

=== 电梯状态转换测试 ===
执行开门操作...
正在开门...
状态已切换为: DoorOpenState
执行关门操作...
正在关门...
状态已切换为: DoorClosedState
执行移动操作...
电梯开始移动...
状态已切换为: MovingState
执行停止操作...
电梯停止中...
状态已切换为: DoorClosedState

=== 测试非法操作 ===
执行开门操作...
正在开门...
状态已切换为: DoorOpenState
执行移动操作...
错误:门还开着,不能移动
状态已切换为: DoorOpenState

Applications avancées du patron d'état

Patron d'état avec état partagé

Dans certains cas, plusieurs contextes peuvent avoir besoin de partager le même objet d'état. Nous pouvons réaliser le partage d'état via le patron Singleton.

Exemple

class Singleton(type):
    """Métaclasse Singleton"""
    _instances = {}
   
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class SharedDoorOpenState(ElevatorState, metaclass=Singleton):
    """État ouvert partagé (Singleton)"""
   
    def open_doors(self):
        print("La porte est déjà ouverte")
        return self
   
    def close_doors(self):
        print("Fermeture de la porte...")
        return SharedDoorClosedState()
   
    def move(self):
        print("Erreur : La porte est encore ouverte, impossible de se déplacer")
        return self
   
    def stop(self):
        print("L'ascenseur s'est arrêté")
        return self

class SharedDoorClosedState(ElevatorState, metaclass=Singleton):
    """État fermé partagé (Singleton)"""
   
    def open_doors(self):
        print("Ouverture de la porte...")
        return SharedDoorOpenState()
   
    def close_doors(self):
        print("La porte est déjà fermée")
        return self
   
    def move(self):
        print("L'ascenseur commence à se déplacer...")
        return SharedMovingState()
   
    def stop(self):
        print("L'ascenseur s'est arrêté")
        return self

class SharedMovingState(ElevatorState, metaclass=Singleton):
    """État de déplacement partagé (Singleton)"""
   
    def open_doors(self):
        print("Erreur : L'ascenseur est en mouvement, impossible d'ouvrir la porte")
        return self
   
    def close_doors(self):
        print("La porte est déjà fermée")
        return self
   
    def move(self):
        print("L'ascenseur est en mouvement")
        return self
   
    def stop(self):
        print("L'ascenseur s'arrête...")
        return SharedDoorClosedState()

Patron État et machines à états

Le patron État est souvent utilisé en combinaison avec des machines à états. Voici un exemple de machine à états plus complexe :

Exemple

class VendingMachineState(ABC):
    """Interface d'état de distributeur automatique"""
   
    @abstractmethod
    def insert_coin(self, machine, amount):
        """Insérer une pièce"""
        pass
   
    @abstractmethod
    def select_product(self, machine, product):
        """Sélectionner un produit"""
        pass
   
    @abstractmethod
    def dispense(self, machine):
        """Distribuer le produit"""
        pass
   
    @abstractmethod
    def refund(self, machine):
        """Remboursement"""
        pass

class WaitingState(VendingMachineState):
    """État en attente de pièce"""
   
    def insert_coin(self, machine, amount):
        print(f"Montant inséré : {amount} yuan")
        machine.balance += amount
        return HasMoneyState()
   
    def select_product(self, machine, product):
        print("Veuillez d'abord insérer une pièce")
        return self
   
    def dispense(self, machine):
        print("Veuillez d'abord insérer une pièce et sélectionner un produit")
        return self
   
    def refund(self, machine):
        print("Aucun montant à rembourser")
        return self

class HasMoneyState(VendingMachineState):
    """État de pièce insérée"""
   
    def insert_coin(self, machine, amount):
        print(f"Continuez à insérer {amount} yuan")
        machine.balance += amount
        return self
   
    def select_product(self, machine, product):
        if product.price <= machine.balance:
            print(f"Produit sélectionné : {product.name}")
            machine.selected_product = product
            return ProductSelectedState()
        else:
            print("Solde insuffisant, veuillez continuer à insérer des pièces ou sélectionner un produit moins cher")
            return self
   
    def dispense(self, machine):
        print("Veuillez d'abord sélectionner un produit")
        return self
   
    def refund(self, machine):
        print(f"Remboursement de {machine.balance} yuan")
        machine.balance = 0
        return WaitingState()

class ProductSelectedState(VendingMachineState):
    """État du produit sélectionné"""
   
    def insert_coin(self, machine, amount):
        print("Produit déjà sélectionné, impossible d'insérer plus de pièces")
        return self
   
    def select_product(self, machine, product):
        print("Produit déjà sélectionné, veuillez d'abord terminer la transaction en cours")
        return self
   
    def dispense(self, machine):
        product = machine.selected_product
        change = machine.balance - product.price
       
        print(f"Distribution : {product.name}")
        if change > 0:
            print(f"Monnaie : {change} yuan")
       
        machine.balance = 0
        machine.selected_product = None
        return WaitingState()
   
    def refund(self, machine):
        print(f"Remboursement de {machine.balance} yuan")
        machine.balance = 0
        machine.selected_product = None
        return WaitingState()

class Product:
    """Classe Product"""
   
    def __init__(self, name, price):
        self.name = name
        self.price = price

class VendingMachine:
    """Distributeur automatique"""
   
    def __init__(self):
        self._state = WaitingState()
        self.balance = 0
        self.selected_product = None
   
    def set_state(self, state):
        self._state = state
   
    def insert_coin(self, amount):
        print(f"Opération : Insérer une pièce de {amount} yuan")
        self.set_state(self._state.insert_coin(self, amount))
   
    def select_product(self, product):
        print(f"Opération : Sélectionner le produit {product.name}")
        self.set_state(self._state.select_product(self, product))
   
    def dispense(self):
        print("Opération : Demander la distribution du produit")
        self.set_state(self._state.dispense(self))
   
    def refund(self):
        print("Operation: Request refund")
        self.set_state(self._state.refund(self))

Avantages et inconvénients du patron État

Avantages

  1. Principe de responsabilité unique: Placer le code lié à un état particulier dans des classes indépendantes
  2. Principe ouvert/fermé: Introduire de nouveaux états sans modifier les classes d'état existantes et le contexte
  3. Éliminer les instructions conditionnelles: Éliminer les énormes instructions conditionnelles de branchement grâce aux appels polymorphes
  4. Des transitions d'état claires: Rendre les transitions d'état plus explicites, réduisant les erreurs causées par les transitions d'état

Inconvénients

  1. Surconception possible: Si le nombre d'états est faible ou change rarement, l'utilisation du patron État peut être trop complexe
  2. Augmentation du nombre de classes d'état: Chaque état nécessite une classe correspondante, ce qui peut entraîner une augmentation du nombre de classes
  3. Couplage entre le contexte et l'état: Le contexte doit connaître toutes les classes d'état concrètes afin d'effectuer les transitions d'état

Exercices pratiques

Exercice 1 : Améliorer le système d'ascenseur

Essayez d'ajouter les fonctionnalités suivantes au système d'ascenseur :

  • Ajoutez un état "Maintenance", dans lequel toutes les opérations sont interdites
  • Ajoutez une fonctionnalité de sélection d'étage, où les étages ne peuvent être sélectionnés que dans l'état arrêté
  • Implémentez des transitions d'état automatiques pour l'arrivée à l'étage

Exercice 2 : Implémenter un système de feux de circulation

Utilisez le patron State pour implémenter un système de feux de circulation avec trois états : feu rouge, feu vert et feu jaune. Les règles de transition d'état sont les suivantes :

  • Feu rouge → Feu vert (après 30 secondes)
  • Feu vert → Feu jaune (après 25 secondes)
  • Feu jaune → Feu rouge (après 5 secondes)

Exercice 3 : États de personnage de jeu

Concevez un système d'états pour un personnage de jeu, incluant les états suivants :

  • État normal : peut se déplacer et attaquer
  • État empoisonné : perd continuellement de la santé, vitesse de déplacement réduite de moitié
  • État étourdi : ne peut ni se déplacer ni attaquer
  • État invincible : immunisé contre les dégâts

Résumé

Le modèle État est un puissant patron de conception, particulièrement adapté aux scénarios où le comportement d’un objet dépend de son état, et où il existe de nombreux états avec des transitions complexes. En encapsulant chaque état comme une classe indépendante, le modèle État rend le code plus clair, plus facile à maintenir et plus facile à étendre.

Points clés:

  • Le modèle État élimine les instructions conditionnelles en encapsulant les états sous forme d’objets
  • Le contexte délègue le comportement lié à l’état à l’objet d’état actuel
  • Les transitions d’état peuvent être déclenchées par le contexte ou par les objets d’état eux-mêmes
  • Une utilisation raisonnable du modèle État peut grandement améliorer la maintenabilité du code
Autres extensions