PHP – Design Pattern Composite

15 Apr

Out Of Date Warning

Questo post è stato pubblicato più di 2 anni fa (il 15 aprile 2013). Le idee vanno avanti velocemente, le prospettive cambiano quindi i contenuti potrebbero non essere aggiornati. Ti prego di tenere in considerazione questo, e di verificare le informazioni tecniche presenti nell'articolo prima di farne affidamento per i tuoi scopi.

Il Composite è un pattern fondamentale definito dalla GoF e rappresenta un modo semplice di aggregazione e gestione dei gruppi di oggetti simili in modo che per un client un singolo oggetto sia indistinguibile da un insieme di oggetti.

Scopo

Il pattern Composite ci fornisce una singola interfaccia che consente a un client di lavorare in maniera trasparente con un insieme (composizione) di oggetti o con oggetti singoli (foglie). Il codice client può anche non sapere se sta manipolando un oggetto, oppure molti.
In poche parole, le forza di questo pattern sta nel consentirci di manipolare le istanze singole e multiple di un dato componente utilizzando la stessa API.

Problema

Nei casi in cui l’applicazione abbia la necessità di maneggiare una collezione di oggetti, primitivi (foglie) e composti (aggregazione di oggetti), non è auspicabile dover interrogare ogni oggetto per scoprirne il tipo. Il pattern ci permette di evitare che i client siano costretti a fare certe distinzioni.

Partecipanti

L’elemento fondamentale del pattern è una classe astratta che rappresenterà sia gli elementi composti sia le foglie.

  • Component: Classe astratta, dichiara l’interfaccia dei componenti della composizione, implementando un comportamento standard comune per tutte le classi (ove possibile). Il Component dichiara inoltre l’interfaccia di accesso e gestione dei figli ed eventualmente anche dei padri (opzionale).
  • Leaf: Classe concreta che rappresenta gli oggetti figlio (che non possono avere figli) nella composizione.
  • Composite: Classe concreta che rappresenta gli oggetti composti (con figli) nella composizione. Implementerà le operazioni legate alla gestione dei figli (add e remove per esempio) definite dall’interfaccia Component.
  • Client: la parte del programma destinata a manipolare gli oggetti della composizione utilizzando l’interfaccia Component.

Implementazione

Invece di mostrare il classico esempio del file system (che in effetti ben si presta allo scopo) mi sono inventato qualcosa di più originale che ha a che fare con mie ricordi sportivi di gioventù.
Quello che andremo a rappresentare sarà il mondo del basket in cui ipotizziamo la seguente gerarchia:
composite hierarchy

Nel diagramma possiamo notare tre tipi di oggetti (ognuno dei quali sarà rappresentato con apposita classe). Le foglie, cioè gli oggetti semplici privi di figli, saranno rappresentati dalla classe Player, mentre Game ed Team rappresenteranno un’aggregazione di oggetti, le partite sono composte da squadre (due), e le squadre da giocatori.
Metodi

  • add -> aggiunge un figlio
  • remove -> rimuove un figlio
  • getPoints() -> restituisce i punti (di un giocatore, una squadra o una partita)
  • show() -> esegue una rappresentazione dell’oggetto mostrandone il nome ed il punteggio

L’obiettivo che vogliamo raggiungere attraverso il pattern sarà dunque di permettere al client di poter trattare indifferentemente i giocatori, le squadre o le partite attraverso la stessa interfaccia, astraendoci così dal dover conoscere il tipo di oggetto a cui eseguire la richiesta.
Prima di tutto andiamo a dichiarare l’interfaccia, cioè la classe Component, cercando di dare un’implementazione di default ai metodi:

Nell’interfaccia ho definito un’implementazione rivolta ai componenti composti, prevedendo che le foglie li sovrascrivano.

La classe Component dovrebbe definire il maggior numero possibile di metodi comuni alle classi Composite e Leaf, fornendone anche un’implementazione di default che poi sarà opportunamente sovrascritta da foglie e componenti aggregati.

Tralasciando per il momento i metodi add e remove di cui discuteremo dopo, da notare invece l’utilizzo della ricorsione sugli altri metodi.
E’ infatti la composizione ricorsiva il mezzo fondamentale che permetterà ai client di non fare distinzioni tra composti e foglie.
Nei componenti composti l’attributo $_components (un array) molto semplicemente conterrà l’elenco dei figli. I metodi getPoints() e sow() non faranno altro che ciclare ricorsivamente sui figli.

Nelle due classi composte c’è poco da dire, se non che ereditano da Component (Basket).
Vediamo ora i giocatori, cioè i componenti primitivi.

Sui figli, chiaramente non utilizzeremo l’implementazione di default, ma getPoints() e show() restituiranno direttamente i rispettivi valori, senza dover lavorare sulla gerarchia.

Trasparenza vs Sicurezza

Un principio generale della programmazione gerarchica è che una classe dovrebbe definire soltanto i metodi significativi per le sottoclassi. A questo punto diviene palese che Component definisce invece dei metodi (add e remove) che non hanno niente a che fare con le foglie.
Alla domanda se è giusto definire tali metodi nella classe Component (interfaccia) oppure dichiararli soltanto nella classe Composite dobbiamo eseguire una scelta in termini di trasparenza e sicurezza.

  • Trasparenza: La definizione dei metodi di gestione dei figli all’interno dell’interfaccia Component ci darà uno svantaggio in termini di sicurezza, visto che saremo in grado di eseguire operazioni completamente senza senso come l’aggiunta e la rimozione di figli all’interno delle classi Leaf. Tuttavia ci darà una maggiore trasparenza consentendo al client di trattare tutti i componenti in modo uniforme.
  • Sicurezza: La definizione dei metodi di gestione dei figli all’interno della classe Composite ci darà maggiore sicurezza dato che non sarebbe più possibile aggiungere o rimuovere figli nelle foglie, ma perderemo in trasparenza visto che Leaf e Composite avranno interfacce diverse.

Aumentiamo la sicurezza

Tornando al nostro esempio non v’è dubbio che abbiamo dato precedenza alla trasparenza, ma ci siamo esposti al pericolo di poter aggiungere figli alle foglie:

Il codice precedente evidenzia un comportamento totalmente illogico come l’aggiunta di una squadra all’interno di un giocatore, ma possibile visto che l’interfaccia consente questo comportamento.

Esistono varie possibilità, tuttavia un design spesso utilizzato, che ci dà anche un giusto compromesso tra trasparenza e sicurezza, è quello che le operazioni add e remove abbiano un comportamento di default che porta al fallimento (di solito sollevando un’eccezione).

Ora l’interfaccia Component prevede come comportamento di default il fallimento dei metodi di gestione dei figli, naturalmente ora tutte le classi Composite dovranno implementare tali metodi per poter gestire i propri figli.

Di solito il vero vantaggio di questo pattern si vede solo dal punto di vista del client, quindi vediamo com’è semplice creare una partita tra due squadre:

Naturalmente vince Boston 😉

Riassumento

Nell’articolo è stato introdotto il pattern Composite, che risulta particolarmente utile quando dobbiamo lavorare con oggetti organizzati in una struttura ad albero.

Tra i benefici della sua applicazione troviamo la flessibilità, dato che tutto il modello condivide un supertipo comune sarà molto facile aggiungere nuovi oggetti composti o foglia senza dover cambiare niente.

Tuttavia, come detto più volte, il vero punto di forza del pattern sta nel fatto che non vi sarà alcuna necessità per un client di distinguere tra un oggetto composto ed un oggetto foglia (eccetto quando si aggiungono nuovi componenti).
Una chiamata a Team::getPoints() causerà (dietro le quinte) una cascata di chiamate delegate, ma al client, il processo e il risultato sono esattamente equivalenti a quelli associati alla chiamata Player::getPoints().

Lascia un commento