Categorie
PHP

PHP – Design Pattern Decorator

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.

[box type=”note”]Nel caso si debba aggiungere una sola responsabilità, non è necessario definire l’interfaccia (intesa anche come classe astratta) Decorator.[/box]

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.

//Component
abstract class Math
{
    abstract function execute();
}

//ConcreteComponent
class StandardMath extends Math
{
    public function execute()
    {
        return 0;
    }
}

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:

//Decorator
abstract class MathDecorator extends Math
{
    protected $_math;

    public function __construct(Math $math)
    {
        $this->_math = $math;
    }
}

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:

//ConcreteDecoratorA
class AddTwoDecorator extends MathDecorator
{
    public function execute()
    {
        return $this->_math->execute() + 2;
    }
}

//ConcreteDecoratorB
class MultiplyTreeDecorator extends MathDecorator
{
    public function execute()
    {
        return $this->_math->execute() * 3;
    }
}

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:

$m = new AddTwoDecorator(
        new MultiplyTreeDecorator(
        new AddTwoDecorator(new StandardMath())
        )
);

echo $m->execute();

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:

interface Window
{
    public function isOpen();
    public function open();
    public function close();
}

class StandardWindow implements Window
{
    protected $_open = false;

    public function isOpen()
    {
        return $this->_open;
    }

    public function open()
    {
        if (!$this->_open) {
            $this->_open = true;
        }
    }

    public function close()
    {
        if ($this->_open) {
            $this->_open = false;
        }
    }
}

class LockedWindow implements Window
{
    protected $_window;

    public function __construct(Window $window)
    {
        $this->_window = $window;
        $this->_window->close();
    }

    public function isOpen()
    {
        return false;
    }

    public function open()
    {
        throw new Exception('Cannot open locked windows');
    }

    public function close()
    {
        $this->_window->close();
    }
}

$w = new LockedWindow(new StandardWindow());
$w->open();

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.
[box type=”note”]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.[/box]

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

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.