Patron de visiteur Python
Le patron Visiteur est un patron de conception comportemental qui vous permet d'ajouter de nouvelles opérations à des classes existantes sans modifier leur structure. En termes simples, le patron Visiteur sépare les algorithmes de la structure d'objets sur laquelle ils opèrent.
Idée centrale
Imaginez que vous êtes à l'hôpital pour un examen médical : vous (le visité) restez immobile, tandis que différents médecins (visiteurs) viennent vers vous pour effectuer des examens. Chaque médecin se concentre sur sa propre spécialité, mais ils examinent tous le même vous.
En programmation, le patron Visiteur fonctionne de manière similaire :
- Les éléments visitésrestent stables et inchangés
- Les visiteurspeuvent ajouter de nouvelles opérations de manière flexible
- Les éléments acceptent les visites des visiteurs, ce qui permet aux visiteurs d'effectuer des opérations sur eux.
Pourquoi avons-nous besoin du patron Visiteur ?
Limites de l'approche traditionnelle
Supposons que nous ayons un système graphique contenant plusieurs formes :
Exemple
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
def perimeter(self):
return 2 * 3.14 * self.radius
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
Voici le problème: Si nous voulons ajouter de nouvelles fonctionnalités, comme calculer le centre de gravité des formes ou exporter au format SVG, nous devons modifier chaque classe de forme. Cela viole le principe Ouvert/Fermé (ouvert à l'extension, fermé à la modification).
Avantages du patron Visiteur
Le patron Visiteur résout ce problème de la manière suivante :
- Facile d'ajouter de nouvelles opérations: Il suffit de créer une nouvelle classe de visiteur sans modifier les classes d'éléments existantes
- Les opérations liées sont gérées de manière centralisée: organisez les opérations liées dans le même visiteur
- La structure des éléments reste stable: les classes d'éléments n'ont pas besoin de modifications fréquentes
Implémentation du patron Visiteur
Structure de base
Comprenons l'implémentation du patron Visiteur à travers un exemple concret :
Exemple
# 1. Définir l'interface d'élément
class ShapeElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
# 2. Définir l'interface du visiteur
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle):
pass
@abstractmethod
def visit_rectangle(self, rectangle):
pass
# 3. Classes d'éléments concrètes
class Circle(ShapeElement):
def __init__(self, radius):
self.radius = radius
def accept(self, visitor):
visitor.visit_circle(self)
class Rectangle(ShapeElement):
def __init__(self, width, height):
self.width = width
self.height = height
def accept(self, visitor):
visitor.visit_rectangle(self)
# 4. Classes de visiteurs concrètes
class AreaCalculator(ShapeVisitor):
def visit_circle(self, circle):
area = 3.14 * circle.radius * circle.radius
print(f"Aire du cercle: {area:.2f}")
return area
def visit_rectangle(self, rectangle):
area = rectangle.width * rectangle.height
print(f"Aire du rectangle: {area:.2f}")
return area
class PerimeterCalculator(ShapeVisitor):
def visit_circle(self, circle):
perimeter = 2 * 3.14 * circle.radius
print(f"Périmètre du cercle : {perimeter:.2f}")
return perimeter
def visit_rectangle(self, rectangle):
perimeter = 2 * (rectangle.width + rectangle.height)
print(f"Périmètre du rectangle : {perimeter:.2f}")
return perimeter
Exemple d'utilisation
Exemple
shapes = [
Circle(5),
Rectangle(4, 6),
Circle(3)
]
# Créer des visiteurs
area_calculator = AreaCalculator()
perimeter_calculator = PerimeterCalculator()
print("=== Calcul de l'aire ===")
for shape in shapes:
shape.accept(area_calculator)
print("\n=== Calcul du périmètre ===")
for shape in shapes:
shape.accept(perimeter_calculator)
Résultat de sortie :
=== 计算面积 === 圆的面积: 78.50 矩形的面积: 24.00 圆的面积: 28.26 === 计算周长 === 圆的周长: 31.40 矩形的周长: 20.00 圆的周长: 18.84
Composants principaux du patron Visitor
1. Visitor (Interface Visitor)
Définit les méthodes de visite pour chaque classe d'élément concret.
Exemple
def visit_circle(self, circle): pass
def visit_rectangle(self, rectangle): pass
def visit_triangle(self, triangle): pass # Peut être facilement étendu
2. ConcreteVisitor (Visiteur concret)
Implémente les opérations définies dans l'interface Visitor.
Exemple
def visit_circle(self, circle):
return f'<circle r="{circle.radius}" />'
def visit_rectangle(self, rectangle):
return f'<rect width="{rectangle.width}" height="{rectangle.height}" />'
3. Élément (Interface Élément)
Définit une méthode pour accepter les visiteurs.
Exemple
@abstractmethod
def accept(self, visitor):
pass
4. ConcreteElement (Élément concret)
Implémente l'interface Element et appelle la méthode du visiteur correspondante dans la méthode accept.
Exemple
def accept(self, visitor):
visitor.visit_circle(self) # C'est ici que se produit le double dispatch
Exemple d'application avancée
Système complexe de traitement de documents
Regardons un exemple plus pratique : le traitement de différents types d'éléments de document.
Exemple
# Interface d'élément de document
class DocumentElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
# Éléments de document concrets
class Paragraph(DocumentElement):
def __init__(self, text):
self.text = text
def accept(self, visitor):
return visitor.visit_paragraph(self)
class Heading(DocumentElement):
def __init__(self, text, level):
self.text = text
self.level = level
def accept(self, visitor):
return visitor.visit_heading(self)
class List(DocumentElement):
def __init__(self, items):
self.items = items
def accept(self, visitor):
return visitor.visit_list(self)
# Interface Visiteur
class DocumentVisitor(ABC):
@abstractmethod
def visit_paragraph(self, paragraph): pass
@abstractmethod
def visit_heading(self, heading): pass
@abstractmethod
def visit_list(self, list_element): pass
# Visiteur concret : export HTML
class HTMLExportVisitor(DocumentVisitor):
def visit_paragraph(self, paragraph):
return f"<p>{paragraph.text}</p>"
def visit_heading(self, heading):
return f"<h{heading.level}>{heading.text}</h{heading.level}>"
def visit_list(self, list_element):
items = "".join(f"<li>{item}</li>" for item in list_element.items)
return f"<ul>{items}</ul>"
# Visiteur concret : nombre de mots
class WordCountVisitor(DocumentVisitor):
def __init__(self):
self.total_words = 0
def visit_paragraph(self, paragraph):
words = len(paragraph.text.split())
self.total_words += words
return words
def visit_heading(self, heading):
words = len(heading.text.split())
self.total_words += words
return words
def visit_list(self, list_element):
words = sum(len(item.split()) for item in list_element.items)
self.total_words += words
return words
# Exemple d'utilisation
document = [
Heading("Tutoriel Python", 1),
Paragraph("Python est un langage de programmation puissant."),
List(["Élément de liste 1", "Élément de liste 2", "Élément de liste 3"]),
Paragraph("Apprendre Python est très intéressant.")
]
# Export HTML
html_visitor = HTMLExportVisitor()
print("=== Export HTML ===")
for element in document:
print(element.accept(html_visitor))
# Nombre de mots
word_visitor = WordCountVisitor()
print("\n=== Nombre de mots ===")
for element in document:
words = element.accept(word_visitor)
print(f"{element.__class__.__name__}: {words} mots")
print(f"Total de mots : {word_visitor.total_words}")
Avantages et inconvénients du patron de conception Visiteur
Avantages
- Principe ouvert/fermé: Facile d'ajouter de nouvelles opérations sans modifier les classes existantes
- Principe de responsabilité unique: Centraliser les comportements associés dans une seule classe de visiteur
- Flexibilité: Différents visiteurs peuvent être sélectionnés à l'exécution
- Séparation des données: Les algorithmes sont séparés des structures de données
Inconvénients
- Difficile de modifier l'interface Element: L'ajout d'une nouvelle classe d'élément nécessite de modifier tous les visiteurs
- Peut briser l'encapsulation: Les visiteurs peuvent avoir besoin d'accéder aux membres privés des éléments
- Complexité: Peut être excessivement complexe pour des structures de données simples
Scénarios applicables
Scénarios adaptés au patron Visiteur
- Structure d'objets stable: Besoin de définir plusieurs opérations sur une structure d'objets relativement stable
- Opérations changeant fréquemment: Besoin d'ajouter fréquemment de nouvelles opérations ou algorithmes
- Opérations associées centralisées: Vouloir regrouper les opérations associées
- Structure d'objets complexe: La structure d'objets contient de nombreux types d'objets différents
Cas d'application concrets
- Compilateurs: Diverses analyses sur les arbres syntaxiques (vérification de types, optimisation de code, etc.)
- Traitement de documents: Effectuer différentes opérations sur les éléments de document (exportation, statistiques, formatage, etc.)
- Systèmes d'interface graphique: Effectuer différentes opérations sur les composants d'interface utilisateur (rendu, gestion des événements, etc.)
- Développement de jeux: Effectuer différentes opérations sur les objets de jeu (détection de collisions, IA, etc.)
Bonnes pratiques et considérations
1. Utilisation de la double répartition
Le cœur du patron Visiteur est la double répartition :
- Première répartition : l'élément accepte le visiteur
- Deuxième répartition : le visiteur visite l'élément concret
Exemple
def accept(self, visitor):
visitor.visit_circle(self) # Ceci détermine la méthode de visite spécifique
2. Gestion de l'état du visiteur
Si le visiteur doit conserver un état :
Exemple
def __init__(self):
self.total_area = 0
self.shape_count = 0
def visit_circle(self, circle):
area = 3.14 * circle.radius * circle.radius
self.total_area += area
self.shape_count += 1
def visit_rectangle(self, rectangle):
area = rectangle.width * rectangle.height
self.total_area += area
self.shape_count += 1
def get_statistics(self):
return {
'total_area': self.total_area,
'shape_count': self.shape_count,
'average_area': self.total_area / self.shape_count if self.shape_count > 0 else 0
}
3. Gestion des exceptions de visite
Exemple
def visit_circle(self, circle):
try:
# Effectuer l'opération
pass
except Exception as e:
print(f"Erreur lors du traitement du cercle : {e}")
def visit_rectangle(self, rectangle):
try:
# Effectuer l'opération
pass
except Exception as e:
print(f"Erreur lors du traitement du rectangle : {e}")
Résumé
Le patron Visiteur est un modèle de conception puissant qui offre une grande extensibilité en séparant les algorithmes des structures de données. Bien qu'il présente une certaine complexité, il peut considérablement améliorer la maintenabilité et l'extensibilité du code dans des scénarios appropriés.
Points clés :
- Le patron Visiteur convient aux scénarios où la structure des objets est stable mais où les opérations changent fréquemment.
- Obtient un comportement polymorphe grâce à un mécanisme de double dispatch.
- Facile d'ajouter de nouvelles opérations, mais difficile d'ajouter de nouveaux types d'éléments.
- Dans des projets réels, pesez sa complexité par rapport aux bénéfices qu'il apporte.