Decoration

Il decorator è un design pattern utilizzato per aggiungere a run-time nuove funzionalità ad oggetti già esistenti. Tale funzionalità viene realizzata costruendo una nuova classe (il decoratore) in grado di “avvolge” l’istanza della classe interessata. Al costruttore della classe decoratore andrà poi passata tale istanza, in modo che essa venga correttamente decorata.

Il pattern decoration rappresenta una valida alternativa all’uso dell’ereditarietà singola o multipla, così come definita all’interno del paradigma OO (Guida al paradigma Object Oriented). Facendo uso dell’ereditarietà è possibile aggiungere staticamente (e solo secondo i legami definiti nella gerarchie di classi) nuove funzionalità alla classe in questione, non è tuttavia possibile ottenere a run-time una combinazione arbitraria di queste funzionalità, né la loro aggiunta/rimozione. Scegliendo di non utilizzare il pattern observer è possibile tentare di giungere al medesimo risultato (e quindi di acquisire le precedentemente citate abilità) utilizzando due diverse strategie.

Nell’esempio sottostante, come nei successivi, considereremo la classe “bevanda”, la quale potrà essere estesa con le decorazioni “caffe”, “latte”, “soia”, eccetera. In ogni momento dovremmo essere in grado di descrivere la bevanda istanziata (utilizzando il metodo descrizione) e di calcolarne il costo in funzione delle sue decorazioni (utilizzando il metodo costo). Utilizzeremo inoltre il linguaggio Java come linguaggio di riferimento.

La prima delle strategie precedentemente citate fa uso del concetto di ereditarietà, si procede inizialmente con l’implementazione della classe base “bevanda” e successivamente si procede con la realizzazione delle classi rappresentanti le diverse combinazioni di bevande e decoratori, ad esempio: caffe, caffeLatte, caffePanna, eccetera. Tale implementazione è dovuta al mancato supporto, da parte del linguaggio Java, dell’ereditarietà multipla. Le diverse classi implementeranno la loro versione del metodo costo e descrizione.

La seconda strategia prevede la realizzazione di una classe bevanda contenente un certo numero di campi booleani, in questo modo sarà possibile specificare la presenza o l’assenza delle diverse decorazioni direttamente all’interno dell’istanza in questione. Questo secondo approccio è molto pesante e costoso, dal momento che esso impone la costruzione di un oggetto contenente le informazioni di tutte le decorazioni disponibili, inoltre esso viola l’OCP (Open Close Principle), il quale afferma che le entità software (classi, moduli, funzioni, ecc.) dovrebbero essere aperte all’estensione, ma chiuse alle modifiche.

Il pattern decoration prevede l’esistenza di un’interfaccia Component all’interno della quale vengono promessi tutti metodi della classe concreta che dovranno poi essere modificati dalle decorazioni. Nel nostro caso questi sono, ad esempio, il metodo costo e il metodo descrizione. L’interfaccia Component viene implementata dalla classe astratta Decorator (tal volta tale classe viene anche implementata in modo concreto) e dalla classe concreta ConcreteComponent. Le istanze della classe ConcreteComponent rappresentano la base delle future decorazioni, nel nostro esempio tale classe concreta è rappresentata dalla classe “caffe”.

La classe astratta Decorator rappresenta, invece, la parte comune a tutti i decoratori, esso comunica che ogni decoratore è a sua volta un componente e che quindi dovrà implementare i metodi previsti da quest’ultimo. La classe astratta Decorator è in relazione di aggregazione con l’interfaccia Component, questo perché ogni decoratore è a sua volta un componente e può quindi a sua volta essere decorato. Esempio: posso mettere del cacao sulla panna che decora il mio caffè, oltre a mettere dello zucchero nel caffè che ho già decorato con la mia panna.

Esisteranno infine, diverse classi ConcreteDecoration con le quali andranno rappresentati i diversi decoratori, le diverse classi ConcreteDecoration dovranno realizzare tutti i metodi promessi dall’interfaccia Component. Nel nostro caso esisterà una classe specifica riservata allo zucchero, una riservata alla soia, alla panna e così via.

Diagramma UML del pattern Decorator:

Utilizzando l’approccio appena descritto sarà possibile implementare un caffè contenente un qualsiasi sottoinsieme di decoratori e successivamente ottenerne facilmente la descrizione e il costo. Ogni componente sarà caratterizzato da un diverso costo, inoltre, ogni decorazione sarà capace di calcolare ricorsivamente il costo degli elementi che decora. Nell’esempio sottostante un caffè dal costo di 99 centesimi e stato decorato con del cacao, dal costo di 20 centesimi, il quale è a sua volta decorato con della panna dal costo di 10 centesimi. Invocando il metodo costo sulla decorazione più esterna (la panna), e sfruttando il processo ricorsivo, si otterrà il calcolo del costo dell’intera bevanda, uguale a 1.29.

Decoration in Java

Utilizzando le classi fornite dall’esempio sarà possibile istanziare un caffè generico e un caffè decorato con la panna.

/* Implemento l'interfaccia Component come una classe astratta */
public abstract class Beverage {
  public abstract String description();
  public abstract int cost();
}

/* ConcreteComponent */
public class Espresso extends Beverage {
  public String description() { return "Espresso";  }
  public int cost() { return 199; }
}

/* Decoration */
public abstract class CondimentDecorator extends Beverage {
  Beverage beverage; // riferimento all'oggetto decorato (bevanda o condimento)
  public abstract String getDescription();
  public abstract int getCost();

  public String description() {
    return beverage.description() + " " + getDescription();
  }
  public int cost() {
    return beverage.cost() + getCost();
  }
}

/* ConcreteDecoration */
public class Mocha extends CondimentDecorator {
  public Mocha(Beverage beverage) {
    this.beverage = beverage;
  }
  public String getDescription() { return "Mocha"; }
  public int getCost() { return 99; }
}

Di seguito è mostrato un esempio di esecuzione del precedente programma.

public class Test {
    public static void main(String[] args) {
        Beverage coffeSimple = new Espresso();
        
        System.out.println(coffeSimple.cost()/100);    // 1.99
        System.out.println(coffeSimple.description()); // Espresso
        
        Beverage coffeMocha = new Mocha(coffeSimple);
        
        System.out.println(coffeMocha.cost());         // 2.98
        System.out.println(coffeMocha.description());  // Espresso Mocha
    }
}

L’approccio del pattern decorator alla risoluzione del problema si dimostra essere quindi più snella e compatta rispetto alle alternative precedentemente citate. Questo dimostra come l’ereditarietà non rappresenti sempre la soluzione migliore, due diversi principi del software design affermano:

  • Se possibile occorre favorire la composizione;
  • Le entità software (classi, moduli, funzioni, ecc.) dovrebbero essere aperte all’estensione, ma chiuse alle modifiche (Open Close Principle).

La classe inputStream è un esempio di pattern Decorator in linguaggio Java.