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 :
- Context (contexte): maintient une instance d'un objet d'état concret ; cette instance définit l'état actuel
- State (interface d'état): définit une interface pour encapsuler le comportement associé à un état particulier du Context
- 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
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
"""É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
"""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
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
"""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
"""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
- Principe de responsabilité unique: Placer le code lié à un état particulier dans des classes indépendantes
- Principe ouvert/fermé: Introduire de nouveaux états sans modifier les classes d'état existantes et le contexte
- Éliminer les instructions conditionnelles: Éliminer les énormes instructions conditionnelles de branchement grâce aux appels polymorphes
- 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
- Surconception possible: Si le nombre d'états est faible ou change rarement, l'utilisation du patron État peut être trop complexe
- 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
- 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