Composite
Design Pattern Composite
Gérer simplement des objets et leurs regroupements
Introduction
Le design pattern Composite est un pattern de conception structurel permettant de représenter des objets organisés sous la forme d’une arborescence. Son objectif est de permettre de manipuler de manière uniforme un objet ou un groupe d’objets, tels que des fichiers et des dossiers, des catégories de produits ou des éléments d’interface graphique.
Ce design pattern est particulièrement pertinent lorsque le domaine métier contient une arborescence et que les mêmes opérations doivent être appliquées aux éléments individuels comme à leurs regroupements.
Problématique
Imaginons un système de fichiers composé de fichiers et de dossiers. Un fichier possède une taille. Un dossier peut contenir des fichiers, mais également d’autres dossiers. Pour obtenir la taille d’un dossier, il faut donc additionner la taille de tous les éléments qu’il contient.
Sans une abstraction commune, le code doit vérifier le type de chaque élément et implémenter lui-même le parcours de l’arborescence. Cette approche rend le code plus complexe et plus difficile à faire évoluer.
Présentation
Le design pattern Composite définit une abstraction commune pour les objets dits simples et les objets capables d’en contenir d’autres. Dans notre exemple, la classe FileItem (représentant un fichier) et la classe DirectoryItem (représentant un dossier) implémentent toutes les deux l’interface IFileSystemItem. Le code utilisant ce design pattern peut alors appeler la même méthode pour obtenir le nombre d'octets, qu’il manipule un fichier ou un dossier :
Un fichier retourne directement sa taille. Un dossier calcule la sienne en demandant à chacun de ses enfants de retourner la sienne.
Conception
Le design pattern Composite est composé des acteurs suivants :
-
Le Component : définit l’abstraction commune aux objets individuels et aux groupes d’objets. Il expose les opérations applicables à tous les éléments de l’arborescence. Dans notre exemple, ce rôle est assuré par l’interface IFileSystemItem.
-
Le Leaf : représente un objet individuel qui ne contient aucun enfant. Dans notre exemple, FileItem remplit ce rôle en représentant un fichier.
-
Le Composite : représente un objet pouvant contenir d’autres composants, qu’il s’agisse de feuilles ou d’autres composites. Dans notre exemple, DirectoryItem remplit ce rôle en représentant un dossier.
-
Le Client : manipule les objets via l’abstraction commune, sans avoir besoin de distinguer s’il s’agit d’un fichier ou d’un dossier. Il peut appeler la méthode GetSize() sur n’importe quel élément de l’arborescence afin d’en obtenir le nombre d'octets total.
Implémentation
L’interface commune IFileSystemItem expose un nom et une méthode permettant d’obtenir le nombre d'octets d’un élément, que ce soit un fichier ou un dossier :
La classe Item ne représente pas un acteur du design pattern : elle constitue seulement un choix d’implémentation pour factoriser des membres communs entre les classes FileItem et DirectoryItem :
public abstract class Item : IFileSystemItem
{
public string Name { get; }
protected Item(string name)
{
Name = name;
}
public abstract long GetSize();
}
La classe FileItem représente une feuille (leaf) dans l'arborescence, qui correspond à un fichier. Elle ne contient aucun enfant :
public class FileItem : Item
{
public long Size { get; }
public FileItem(string name, long size)
: base(name)
{
Size = size;
}
public override long GetSize()
=> Size;
}
La classe DirectoryItem représente un dossier (composite). Elle peut contenir des fichiers et d’autres dossiers :
public class DirectoryItem : Item
{
private List<IFileSystemItem> Items { get; }
public DirectoryItem(string name)
: base(name)
{
Items = [];
}
public void Add(IFileSystemItem item)
{
Items.Add(item);
}
public override long GetSize()
=> Items.Sum(item => item.GetSize());
}
Le calcul de la taille devient récursif lorsqu’un dossier contient d’autres dossiers : chaque DirectoryItem demande à ses enfants leur nombre d'octets, puis les additionne.
Utilisation
Voici l'arborescence que nous allons construire :
Implémentons la création de quelques fichiers et dossiers (le client) :
var invoiceFile = new FileItem("invoice.pdf", 1_500);
var logoFile = new FileItem("logo.png", 2_500);
var archiveFile = new FileItem("archive.zip", 10_000);
var documentsDirectory = new DirectoryItem("Documents");
documentsDirectory.Add(invoiceFile);
var imagesDirectory = new DirectoryItem("Images");
imagesDirectory.Add(logoFile);
var rootDirectory = new DirectoryItem("Root");
rootDirectory.Add(documentsDirectory);
rootDirectory.Add(imagesDirectory);
rootDirectory.Add(archiveFile);
rootDirectory.Add(imagesDirectory);
rootDirectory.Add(archiveFile);
Nous pouvons obtenir la taille d’un fichier ou d’un dossier de la même manière :
Console.WriteLine(invoiceFile.GetSize()); // 1500
Console.WriteLine(documentsDirectory.GetSize()); // 1500
Console.WriteLine(rootDirectory.GetSize()); // 14000
Cette implémentation n’a pas besoin de connaître le type réel de l’élément manipulé, ni de parcourir lui-même l’arborescence. Chaque élément prend en charge le calcul de sa propre taille.
Avantages
Le design pattern Composite présente plusieurs avantages :
-
Il simplifie la manipulation des structures hiérarchiques.
-
Il permet de traiter uniformément les objets individuels et les groupes d’objets.
-
Il limite les tests conditionnels sur les types, grâce à l'utilisation du polymorphisme.
-
Il facilite l’ajout de nouveaux composants implémentant l'interface commune.

