Polar Code 🎭

Command Palette

Search for a command to run...

14
Pièce N°14

CHAPITRE 13 : COMPOSITE

Le plan s'étalait sur la table : un schéma organisationnel. Des boîtes dans des boîtes. Un département contenant des équipes, contenant des employés. Le système devait calculer le coût total du département. La première implémentation était un cauchemar de if (estDepartement) { for (equipe of equipes) { ... } }. Ils traitaient les objets individuels et les groupes différemment. Il fallait une abstraction qui les unifie. Il fallait traiter une feuille et une branche de la même manière. Il fallait un Composite.

Structures arborescentes

La nature, l'organisation, les systèmes informatiques sont pleins d'arbres. Un système de fichiers (dossiers/fichiers), une interface graphique (fenêtres/panneaux/boutons), une structure organisationnelle (départements/équipes/personnes). Le pattern Composite permet de composer des objets dans des structures arborescentes pour représenter des hiérarchies partie-tout. Il laisse les clients traiter les objets individuels et les compositions d'objets de manière uniforme.

Uniformité objet / groupe

Le génie du Composite est de définir une interface commune (Component) pour les objets primitifs (Leaf) et les conteneurs (Composite). Ainsi, un client peut appeler une opération (comme afficher(), calculerCout()) sur n'importe quel élément de l'arbre, sans se soucier de savoir s'il s'agit d'un élément unique ou d'un groupe.

// L'interface commune qui définit le comportement attendu.
interface ComposantSystemeFichiers {
    obtenirTaille(): number;
    afficher(indentation?: string): void;
}

// --- FEUILLE (Leaf) : Représente un objet terminal (pas d'enfants).
class Fichier implements ComposantSystemeFichiers {
    constructor(private nom: string, private taille: number) {}

    obtenirTaille(): number {
        // Pour un Fichier, c'est immédiat.
        return this.taille;
    }

    afficher(indentation: string = ''): void {
        console.log(`${indentation}📄 ${this.nom} (${this.taille} octets)`);
    }
}

// --- COMPOSITE : Représente un conteneur d'autres composants.
class Dossier implements ComposantSystemeFichiers {
    private enfants: ComposantSystemeFichiers[] = [];

    constructor(private nom: string) {}

    // Méthodes de gestion de la structure.
    ajouter(composant: ComposantSystemeFichiers): void {
        this.enfants.push(composant);
    }

    supprimer(composant: ComposantSystemeFichiers): void {
        const index = this.enfants.indexOf(composant);
        if (index > -1) {
            this.enfants.splice(index, 1);
        }
    }

    // Implémentation de l'interface commune.
    obtenirTaille(): number {
        // Pour un Dossier, on somme récursivement la taille des enfants.
        let total = 0;
        for (const enfant of this.enfants) {
            total += enfant.obtenirTaille(); // Appel polymorphique !
        }
        return total;
    }

    afficher(indentation: string = ''): void {
        console.log(`${indentation}📁 ${this.nom}/`);
        // Affiche récursivement chaque enfant, avec une indentation accrue.
        for (const enfant of this.enfants) {
            enfant.afficher(indentation + '  ');
        }
    }
}

Cas d'usage

1. Interface Utilisateur (UI) :

interface Widget {
    rendre(): void;
}

class Bouton implements Widget { rendre() { console.log("Rendu d'un bouton"); } }
class ZoneTexte implements Widget { rendre() { console.log("Rendu d'une zone de texte"); } }

class Conteneur implements Widget {
    private enfants: Widget[] = [];
    ajouter(w: Widget) { this.enfants.push(w); }
    rendre() {
        console.log("Rendu du conteneur, puis de ses enfants:");
        this.enfants.forEach(enfant => enfant.rendre());
    }
}

// Utilisation
const fenetre = new Conteneur();
fenetre.ajouter(new Bouton());
const panneau = new Conteneur();
panneau.ajouter(new ZoneTexte());
panneau.ajouter(new Bouton());
fenetre.ajouter(panneau);

fenetre.rendre();
// Rendu du conteneur, puis de ses enfants:
// Rendu d'un bouton
// Rendu du conteneur, puis de ses enfants:
// Rendu d'une zone de texte
// Rendu d'un bouton

2. Système de Permissions / Rôles :

interface Permission {
    aAcces(resource: string): boolean;
}

class PermissionSimple implements Permission {
    constructor(private resource: string) {}
    aAcces(resource: string): boolean { return this.resource === resource; }
}

class GroupePermissions implements Permission {
    private permissions: Permission[] = [];
    ajouter(p: Permission) { this.permissions.push(p); }
    aAcces(resource: string): boolean {
        // L'accès est accordé si AU MOINS UNE permission du groupe l'accorde.
        return this.permissions.some(p => p.aAcces(resource));
    }
}

// Un utilisateur a un groupe de permissions, qui peut contenir d'autres groupes.
const accesFichiers = new GroupePermissions();
accesFichiers.ajouter(new PermissionSimple('lire_fichier'));
accesFichiers.ajouter(new PermissionSimple('ecrire_fichier'));

const accesAdmin = new GroupePermissions();
accesAdmin.ajouter(new PermissionSimple('supprimer_utilisateur'));
accesAdmin.ajouter(accesFichiers); // Inclusion d'un autre groupe !

console.log(accesAdmin.aAcces('lire_fichier')); // true
console.log(accesAdmin.aAcces('supprimer_utilisateur')); // true

Mise en garde et considérations

  1. Transparence vs Sécurité : L'interface commune (ComposantSystemeFichiers) expose-t-elle les méthodes de gestion des enfants (ajouter, supprimer) ? Si oui, c'est transparent (le client peut tout faire), mais dangereux (un client peut essayer d'ajouter un enfant à une Feuille). Si non, c'est sûr, mais le client doit faire un if pour savoir s'il a affaire à un Composite. Le GoF penche pour la transparence, en laissant les Feuilles implémenter ajouter avec une erreur ou un no-op. C'est un compromis.
  2. Ordre des enfants : Les Composites peuvent avoir besoin de gérer l'ordre (ex: mise en page). L'interface peut ajouter des méthodes comme inserer(enfant, index).
  3. Cache des opérations coûteuses : Si une opération comme obtenirTaille() est coûteuse et souvent appelée, le Composite peut mettre en cache le résultat et l'invalider quand la structure change.
  4. Parcours et itérateurs : Pour parcourir l'arbre, on peut utiliser le pattern Iterator en plus du Composite.
  5. Désavantage : Concevoir une interface commune peut parfois forcer à définir des méthodes qui n'ont pas de sens pour toutes les sous-classes (comme ajouter sur une Feuille). Il faut bien peser le besoin d'uniformité.

Je repliai le plan organisationnel. Le Composite était plus qu'un simple pattern. C'était une façon de penser les hiérarchies. Il transformait une structure complexe en une entité unique sur laquelle on pouvait appliquer des opérations globales, récursivement.

Il était élégant, puissant, mais pas une solution miracle. Il fallait que la relation "partie-tout" soit claire, et que l'uniformité du traitement ait un sens métier.

Le prochain pattern, le Decorator, était d'une nature différente. Là où le Composite agrégeait des objets pour former un arbre, le Decorator les enveloppait pour ajouter des responsabilités dynamiquement, comme des couches successives. Une autre manière de composer, mais dans la dimension des fonctionnalités, pas de la structure.