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

class Circle:
    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

from abc import ABC, abstractmethod

# 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

# Créer des objets de forme
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

class ShapeVisitor(ABC):
    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

class ExportVisitor(ShapeVisitor):
    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

class ShapeElement(ABC):
    @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

class Circle(ShapeElement):
    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

from abc import ABC, abstractmethod

# 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

  1. Principe ouvert/fermé: Facile d'ajouter de nouvelles opérations sans modifier les classes existantes
  2. Principe de responsabilité unique: Centraliser les comportements associés dans une seule classe de visiteur
  3. Flexibilité: Différents visiteurs peuvent être sélectionnés à l'exécution
  4. Séparation des données: Les algorithmes sont séparés des structures de données

Inconvénients

  1. Difficile de modifier l'interface Element: L'ajout d'une nouvelle classe d'élément nécessite de modifier tous les visiteurs
  2. Peut briser l'encapsulation: Les visiteurs peuvent avoir besoin d'accéder aux membres privés des éléments
  3. Complexité: Peut être excessivement complexe pour des structures de données simples

Scénarios applicables

Scénarios adaptés au patron Visiteur

  1. Structure d'objets stable: Besoin de définir plusieurs opérations sur une structure d'objets relativement stable
  2. Opérations changeant fréquemment: Besoin d'ajouter fréquemment de nouvelles opérations ou algorithmes
  3. Opérations associées centralisées: Vouloir regrouper les opérations associées
  4. 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

# Dans l'élément
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

class StatisticsVisitor(ShapeVisitor):
    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

class SafeVisitor(ShapeVisitor):
    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.
Autres extensions