DEV Community

Cover image for Composite Pattern: trattare gerarchie come singoli oggetti
Dev-Iadicola
Dev-Iadicola

Posted on • Originally published at iadicola.it

Composite Pattern: trattare gerarchie come singoli oggetti

Il problema: strutture ricorsive con trattamento diverso

Un menu di navigazione ha voci semplici (link a una pagina) e voci con sotto-menu (dropdown che contengono altre voci). Il codice che renderizza il menu deve distinguere costantemente: "questa voce e un link diretto o un contenitore?" — e dentro il contenitore, ripetere la stessa domanda per ogni elemento. Ogni rendering, ogni conteggio, ogni ricerca richiede un if (is_array($item)) o un instanceof per distinguere foglie da nodi.

Lo stesso problema emerge ovunque ci siano strutture ad albero: un filesystem (file e cartelle), un'organizzazione aziendale (dipendenti e dipartimenti), un preventivo (singole voci e raggruppamenti), un layout UI (componenti singoli e container). Il Composite Pattern elimina la distinzione: foglie e nodi implementano la stessa interfaccia, e il codice client li tratta allo stesso modo.

Cos'e il Composite Pattern: definizione formale

Il Gang of Four definisce il Composite come un pattern strutturale che "compone oggetti in strutture ad albero per rappresentare gerarchie parte-intero. Il Composite permette ai client di trattare oggetti singoli e composizioni di oggetti in modo uniforme". La struttura prevede:

  • Component: l'interfaccia comune a foglie e compositi (es. MenuItemInterface con render(): string)
  • Leaf: l'elemento terminale che non contiene figli (es. MenuItem — un singolo link)
  • Composite: l'elemento che contiene figli, anch'esso Component (es. MenuGroup — un dropdown con sotto-voci)

La chiave e che MenuGroup implementa MenuItemInterface e contiene un array di MenuItemInterface. Il suo metodo render() chiama render() su ogni figlio. I figli possono essere MenuItem (foglie) o altri MenuGroup (compositi) — la ricorsione e naturale.

Esempio teorico: un sistema di preventivi

Un preventivo per un progetto web ha una struttura gerarchica:

  • Progetto Completo (composito, 15.000 EUR totale)
    • Frontend (composito, 6.000 EUR)
      • Design UI — 2.000 EUR (foglia)
      • Sviluppo template — 2.500 EUR (foglia)
      • Responsive testing — 1.500 EUR (foglia)
    • Backend (composito, 7.000 EUR)
      • API REST — 3.000 EUR (foglia)
      • Autenticazione — 2.000 EUR (foglia)
      • Pannello admin — 2.000 EUR (foglia)
    • Deploy & Infrastruttura (composito, 2.000 EUR)
      • Setup server — 1.000 EUR (foglia)
      • CI/CD pipeline — 1.000 EUR (foglia)

L'interfaccia QuoteItemInterface ha due metodi: getTotal(): float e getDescription(): string. La foglia QuoteLine restituisce il proprio importo. Il composito QuoteGroup somma i totali di tutti i figli. Chiamare $progetto->getTotal() calcola ricorsivamente il totale dell'intero preventivo, indipendentemente dalla profondita della gerarchia.

Operazioni uniformi sulla gerarchia

Se aggiungi un metodo applyDiscount(float $percentage): void all'interfaccia, la foglia applica lo sconto al proprio importo, e il composito delega ad ogni figlio. Uno sconto applicato al nodo "Backend" si propaga automaticamente a "API REST", "Autenticazione" e "Pannello admin". Uno sconto applicato al nodo radice si propaga all'intero preventivo. Il codice che applica lo sconto non sa e non deve sapere se sta operando su una foglia o su un albero di 50 nodi.

Esempio teorico: un filesystem virtuale

L'esempio classico del Composite e il filesystem. L'interfaccia FileSystemEntry dichiara: getSize(): int, getName(): string, getPath(): string. La foglia File restituisce la propria dimensione. Il composito Directory somma le dimensioni di tutti i figli (file e sotto-directory). Chiamare $rootDir->getSize() calcola ricorsivamente la dimensione dell'intero albero.

Aggiungere operazioni e semplice: find(string $name): array nella foglia controlla il proprio nome, nella directory delega a tutti i figli e raccoglie i risultati. delete(): void nella foglia rimuove il file, nella directory rimuove ricorsivamente tutto il contenuto e poi se stessa. Ogni operazione si definisce una volta per la foglia e una volta per il composito.

Composite e il rendering di UI

Nei sistemi di rendering UI, il Composite Pattern e onnipresente. Un Panel contiene Button, Label, TextField e altri Panel. Il metodo render() di un Panel renderizza tutti i figli in sequenza. Il metodo setVisible(false) nasconde il panel e tutti i suoi figli. Il metodo validate() valida tutti i campi contenuti, ricorsivamente.

React, Vue, Blade — tutti i sistemi di templating moderni usano il Composite Pattern implicitamente: un componente può contenere altri componenti, e il rendering e ricorsivo. La differenza e che nel Composite Pattern classico l'interfaccia e esplicita e tipizzata, mentre nei template engine e implicita nella struttura del markup.

Quando usare il Composite Pattern

  • Usa il Composite quando i dati hanno una struttura ad albero naturale (menu, filesystem, organizzazioni, preventivi, UI)
  • Usa il Composite quando vuoi trattare singoli elementi e gruppi di elementi allo stesso modo
  • Usa il Composite quando le operazioni devono propagarsi ricorsivamente nella gerarchia
  • Non usare il Composite se la struttura e piatta: non forzare un albero dove basta una lista
  • Non usare il Composite se foglie e compositi hanno comportamenti radicalmente diversi: il Composite funziona quando l'interfaccia comune ha senso per entrambi

Il Composite Pattern trasforma la complessità strutturale in semplicità d'uso: una gerarchia di qualsiasi profondita si usa come un singolo oggetto. E uno dei pattern che meglio dimostrano come la ricorsione e il polimorfismo, combinati, possano gestire complessità che sembrerebbe richiedere logica ad hoc.


👉 Leggi l'articolo completo su iadicola.it

Top comments (0)