PHP – Design Pattern Decorator

29 Apr

oop decorator pattern

Out Of Date Warning

Questo post è stato pubblicato più di 2 anni fa (il 29 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.

Nell’object-oriented programming il pattern Decorator è un design pattern strutturale, che ci permette di aggiungere funzionalità ad un oggetto dinamicamente in fase di runtime.
Detto anche Wrapper è di uno dei pattern fondamentali definiti dalla GoF.

Obiettivo

Un modo immediato per aggiungere funzionalità (o responsabilità come dicono quelli della GoF) è utilizzare l’ereditarietà.
Supponiamo che ad un oggetto di tipo Testo vogliamo aggiungere un bordo. Possiamo creare una classe TestoBordato che eredita da Testo a che restituirà il testo con bordo.

Tuttavia come abbiamo ormai imparato, l’ereditarietà, ove possibile è meglio evitarla. In primo luogo è statica (nuove responsabilità sarebbero aggiunte in fase di compilazione), inoltre tende ad aumentare la complessità generando infinite gerarchie che divengono difficili da gestire.

L’obiettivo del pattern Decorator è dunque quello di poter applicare nuove funzionalità in modo flessibile, racchiudendo il componente da decorare (Testo) in un’altro responsabile dell’aggiunta della funzionalità (il bordo).
Questo tipo di approccio ci permetterà inoltre di poter combinare più decoratori per ogni caso in modo tale di non essere bloccati con un solo decoratore per istanza. E’ difatti possibile annidare ricorsivamente i decoratori permettendoci così di aggiungere un numero illimitato di funzionalità.

Partecipanti

  • Component: definisce l’interfaccia dell’oggetto a cui verranno aggiunte nuove funzionalità.
  • ConcreteComponent: definisce l’oggetto concreto al quale aggiungere le funzionalità.
  • Decorator: mantiene un riferimento all’oggetto Component e definisce un’interfaccia conforme all’interfaccia di Component.
  • ConcreteDecorator: aggiunge le funzionalità al componente.
Nel caso si debba aggiungere una sola responsabilità, non è necessario definire l’interfaccia (intesa anche come classe astratta) Decorator.

Struttura

decorator structure
Come si nota dal diagramma, sia ConcreteComponent, cioè l’oggetto al quale si vuole (eventualmente) aggiungere funzionalità, sia i decoratori ereditano dalla solita interfaccia.
Possiamo inoltre notare la relazione di aggregazione tra Component e Decorator. Questo vuol dire che sarà il decoratore a contenere un riferimento di Component al proprio interno e non viceversa.
I più attenti avranno notato un comportamento inverso rispetto al design pattern strategy in cui erano le strategie contenute nel contesto.

Esempio con classe astratta

Prima di partire, è importante comprendere che la classe Decorator (decoratore) avrà la stessa API dell’oggetto avvolto. Avvolgendo l’oggetto e sostituendo le chiamate a tale oggetto, con le chiamate al decoratore, il decoratore è in grado di modificare il comportamento dell’oggetto.
Per chiarire questo, diamo un’occhiata a un decoratore d’esempio. Inizieremo con la creazione della classe che sarà decorata.

Qua abbiamo creato una classe astratta Math, che è usata per definire le API dell’oggetto, ed una classe concreta base che vogliamo decorare.
Ora creiamo la nostra classe decoratore astratta, che sarà usata per creare quella del decoratore concreto:

Il costruttore del decoratore astratto accetta un parametro $math, che viene utilizzato per passare l’oggetto avvolto.
Da notare che attraverso il type hinting ci andremo ad assicurare che l’oggetto passato sia di tipo Component, e cioè implementi la sua interfaccia.
Ora possiamo creare i nostri decoratori:

Ed infine ecco come grazie all’interfaccia comune tra decorato e decoratori, per il client sia del tutto trasparente la presenza o meno del decoratore:

L’esempio outputterà 8 (cioè 0 + 2 * 3 + 2).

Esempio con interfaccia

Chi lavora oppure ha lavorato con ZF1 si sarà accorto che il pattern decorator è stato implementato per modellare Zend_Form ed i sui elementi.

Prendo quindi spunto dalla sua documentazione per mostrare un’altro esempio di utilizzo del modello. Questa volta l’interfaccia non sarà una classe astratta bensì un’interfaccia vera e propria.

Dal punto di vista del pattern direi che non c’è una grande differenza, all’interfaccia naturalmente mancherà un’implementazione di base, ma saremo altrettanto sicuri che tutte le classi che la implementano dovranno rispettare il contratto.

Partiamo quindi con il definire un’interfaccia comune, che sarà implementata sia dall’oggetto di origine che dai decoratori:

Creiamo poi un oggetto di tipo StandardWindow e lo passiamo al costruttore di LockedWindow che nel nostro caso rappresenta il decoratore.
Non avremo bisogno di implementare nessuna sorta di funzionalità di “bloccaggio” della finestra in StandardWindow, ma sarà il decoratore ad aggiungere tale funzionalità.
Potremo usare la nostra finestra chiusa (che sarà un’istanza del decoratore) ovunque, come fosse una finestra standard.

Per la cronaca ZF2, a quanto ho capito, ha rimosso i decoratori per la gestione delle form. Chi come me si fosse cimentato nella loro personalizzazione si sarebbe certo accorto che erano un mezzo pasticcio.

Applicabilità

L’utilizzo del pattern decorator potrebbe ritornare utile quando:

  • Si vuole aggiungere funzionalità ai singoli oggetti dinamicamente ed in maniera trasparente.
  • Si vuole togliere responsabilità agli oggetti, rendendoli quindi più snelli.
  • Si vuole evitare di utilizzare l’ereditarietà. I decoratori forniscono infatti un’alternativa flessibile alla creazione di sottoclassi per estendere le funzionalità.

Conclusioni

Nell’articolo è stato introdotto il pattern decorator il quale ha come obiettivo quello di consentire l’aggiunta di ulteriori funzionalità (responsabilità) ad un oggetto dinamicamente.

Uno degli aspetti fondamentali del modello e quello di consentire ai decoratori di apparire in tutti i punti in cui può apparire l’oggetto da decorare. In questo modo i client, non si accorgeranno della differenza tra un componente decorato ed uno non decorato, risultando così indipendenti dalla decorazione.

E’ preferibile che Component sia il più leggera possibile, probabilmente utilizzando le interfacce piuttosto che le classi astratte siamo più sicuri che si focalizzi sulla definizione di un’interfaccia piuttosto che sulla memorizzazione di dati. Un questo modo evitiamo di rendere i decoratori troppo pesanti visto che potrebbero essere utilizzati in quantità.

Lascia un commento