Patron Composite en Python

Le patron Composite est un patron de conception structurel qui permet de composer des objets en structures arborescentes pour représenter des hiérarchies partie-tout. Le patron Composite permet aux clients de traiter uniformément les objets individuels et les compositions d'objets, sans avoir à se soucier s'ils ont affaire à un objet individuel ou à l'ensemble de la structure composite.

Analogie avec la vie réelle

Imaginez la structure organisationnelle d'un système de fichiers :

  • Fichier individuel: est l'unité la plus élémentaire
  • Dossier: peut contenir plusieurs fichiers ou d'autres dossiers
  • Que vous opériez sur un fichier ou un dossier, vous pouvez effectuer des opérations telles que « ouvrir », « supprimer », « renommer », etc.

Cette relation hiérarchique « partie-tout » est un scénario d'application typique du patron Composite.


Composants principaux du patron Composite

1. Interface de composant (Component)

C'est le cœur du patron Composite, définissant une interface commune pour tous les objets (y compris les nœuds feuilles et les nœuds composites).

Exemple

from abc import ABC, abstractmethod
from typing import List

class FileSystemComponent(ABC):
    """Classe de base abstraite d'un composant de système de fichiers"""
   
    def __init__(self, name: str):
        self.name = name
        self.parent = None
   
    @abstractmethod
    def display(self, indent: int = 0) -> None:
        """Afficher les informations du composant"""
        pass
   
    @abstractmethod
    def get_size(self) -> int:
        """Obtenir la taille du composant"""
        pass
   
    def get_path(self) -> str:
        """Obtenir le chemin complet"""
        if self.parent:
            return f"{self.parent.get_path()}/{self.name}"
        return self.name

2. Nœud feuille (Leaf)

Représente les objets feuilles dans la composition ; les nœuds feuilles n'ont pas de nœuds enfants.

Exemple

class File(FileSystemComponent):
    """Classe File - nœud feuille"""
   
    def __init__(self, name: str, size: int):
        super().__init__(name)
        self._size = size
   
    def display(self, indent: int = 0) -> None:
        """Afficher les informations du fichier"""
        spaces = "  " * indent
        print(f"{spaces}📄 {self.name} ({self._size} bytes)")
   
    def get_size(self) -> int:
        """Renvoie la taille du fichier"""
        return self._size

3. Nœud composite (Composite)

Représente des objets composites pouvant contenir des composants enfants, définissant des méthodes pour stocker les composants enfants.

Exemple

class Directory(FileSystemComponent):
    """Classe Directory - nœud composite"""
   
    def __init__(self, name: str):
        super().__init__(name)
        self._children: List[FileSystemComponent] = []
   
    def add(self, component: FileSystemComponent) -> None:
        """Ajouter un composant enfant"""
        component.parent = self
        self._children.append(component)
   
    def remove(self, component: FileSystemComponent) -> None:
        """Supprimer le composant enfant"""
        self_children.remove(component)
        component.parent = None
   
    def display(self, indent: int = 0) -> None:
        """Afficher le répertoire et tous ses composants enfants"""
        spaces = "  " * indent
        {spaces}📄 {self.name}/(f"{spaces}📁 {self.name}/")
       
        # Afficher récursivement tous les composants enfants
        for child in self_children:
            child.display(indent + 1)
   
    def get_size(self) -> int:
        """Afficher la taille du répertoire (y compris tous les composants enfants)"""
        total_size = 0
        for child in self_children:
            total_size += child.get_size()
        return total_size
   
    def find_component(self, name: str) -> FileSystemComponent:
        """Trouver un composant par le nom spécifié"""
        for child in self_children:
            if child.name == name:
                return child
            if isinstance(child, Directory):
                found = child.find_component(name)
                if found:
                    return found
        return None

Exemple complet : simulation de système de fichiers

Démontrons l'application pratique du patron Composite avec un exemple complet :

Exemple

def demonstrate_composite_pattern():
    """Démontrer l'utilisation du patron Composite"""
   
    # Créer le répertoire racine
    root = Directory("root")
   
    # Créer un sous-répertoire
    documents = Directory("documents")
    pictures = Directory("pictures")
    music = Directory("music")
   
    # Créer des fichiers
    readme = File("README.txt", 1024)
    notes = File("notes.md", 2048)
    photo1 = File("vacation.jpg", 1536000)
    photo2 = File("family.png", 2048000)
    song1 = File("song1.mp3", 4096000)
    song2 = Fichier("song2.mp3", 5120000)
   
    # Construire la structure de répertoires
    root.add(readme)
    root.add(documents)
    root.add(pictures)
    root.add(music)
   
    documents.add(notes)
   
    pictures.add(photo1)
    pictures.add(photo2)
   
    music.add(song1)
    music.add(song2)
   
    # Afficher toute la structure du système de fichiers
    print("=== Structure du système de fichiers ===")
    root.display()
   
    print("\n=== Statistiques de taille ===)
    print(f"Taille totale du répertoire racine : {root.get_size()} bytes")
    print(f"Taille du répertoire Pictures : {pictures.get_size()} bytes")
    print(f"Taille du répertoire Music : {music.get_size()} bytes")
   
    print("\n=== Informations sur le chemin ===)
    print(f"Chemin du fichier : {photo1.get_path()}")
    print(f"Chemin du répertoire : {pictures.get_path()}")
   
    print("\n=== Rechercher un composant ===)
    found = root.find_component("song1.mp3")
    if found:
        print(f"Fichier trouvé : {found.get_path()}")

# Exécuter la démo
if __name__ == "__main__":
    demonstrate_composite_pattern()

L'exécution du code ci-dessus produira la sortie suivante :


Diagramme de classes UML du patron Composite


Avantages et inconvénients du patron Composite

Avantages

  1. Interface unifiée: Les clients peuvent utiliser des objets individuels et des objets composites de manière cohérente
  2. Principe ouvert/fermé: Il est facile d'ajouter de nouveaux types de composants sans modifier le code existant
  3. Code client simplifié: Les clients n'ont pas besoin de déterminer s'ils ont affaire à un objet individuel ou à un composite
  4. Hiérarchie flexible: Permet de construire des structures arborescentes complexes

Inconvénients

  1. Complexité de conception: Une conception trop généralisée peut rendre le système complexe
  2. Problèmes de sécurité de type: Dans certains cas, une vérification de type à l'exécution peut être nécessaire
  3. Considérations de performance: Pour les grandes hiérarchies, les opérations récursives peuvent affecter les performances

Scénarios d'application pratiques

1. Système de composants d'interface graphique

Exemple

class UIComponent:
    """Classe de base du composant UI"""
    def render(self):
        pass
    def add(self, component):
        pass

class Button(UIComponent):
    """Button - nœud feuille"""
    def render(self):
        print("Rendu du bouton")

class Panel(UIComponent):
    """Panel - nœud composite"""
    def __init__(self):
        self.children = []
   
    def add(self, component):
        self.children.append(component)
   
    def render(self):
        print("Début du rendu du panneau")
        for child in self.children:
            child.render()
        print("Fin du panneau de rendu")

2. Gestion de la structure organisationnelle

Exemple

class Employee:
    """Classe de base Employee"""
    def get_salary(self):
        pass

class Developer(Employee):
    """Developer - nœud feuille"""
    def __init__(self, salary):
        self.salary = salary
   
    def get_salary(self):
        return self.salary

class Department(Employee):
    """Département - nœud composite"""
    def __init__(self):
        self.employees = []
   
    def add(self, employee):
        self.employees.append(employee)
   
    def get_salary(self):
        return sum(emp.get_salary() for emp in self.employees)

Exercices pratiques

Exercice 1 : Étendre le système de fichiers

Ajoutez les fonctionnalités suivantes au système de fichiers :

  1. Implémenter la fonctionnalité de copie de fichiers
  2. Ajouter une recherche filtrée par type de fichier
  3. Implémenter un affichage limité à la profondeur du répertoire

Exercice 2 : Conception du système de menus

Concevez un système de menu de restaurant avec les exigences suivantes :

  • Un menu peut contenir des plats ou des sous-menus
  • Pouvoir calculer le prix total
  • Prendre en charge le filtrage des plats par catégorie

Exercice 3 : Organigramme

Créez un système de structure organisationnelle d'entreprise :

  • Les employés peuvent être des individus ou des services
  • Les services peuvent contenir d'autres employés ou des sous-services
  • Pouvoir calculer le nombre total de personnes et le salaire total d'un service

Résumé

Le patron Composite gère les objets individuels et les compositions d'objets via une interface unifiée, ce qui rend le code client plus concis et flexible. Ce patron est particulièrement adapté aux scénarios avec des structures hiérarchiques, comme les systèmes de fichiers, les composants GUI, les structures organisationnelles, etc.

Points clés:

  • L'interface de composant définit les opérations communes à tous les objets
  • Les nœuds feuilles implémentent les fonctionnalités de base
  • Les nœuds composites gèrent les composants enfants et fournissent des opérations d’agrégation
  • Les clients n’ont pas besoin de faire la distinction entre les objets individuels et les compositions d’objets
Autres extensions