DECORATOR PATTERN

D

introduzione

Ciao Coders,

dopo un primo sguardo nei precedenti articoli ai pattern creazionali più noti, oggi vi parlo di uno dei pattern strutturali più utilizzati: il Decorator. Quando è stata introdotta la programmazione orientata a oggetti, l’ereditarietà era il modello principale utilizzato per estendere la funzionalità dell’oggetto. Non sempre però è l’approccio più giusto. Infatti è stato dimostrato che estendere gli oggetti usando l’ereditarietà spesso si traduce in una gerarchia di classi che esplode, fenomeno noto come Exploding class hierarchy. Giusto per farvi capire il fenomeno della Exploding class hierarchy voglio riportarvi uno schema di classi trovato sul web per farvi capire quanto è poco comprensibile il dominio delle classi quando la gerarchia diventa grande.

Unico mio commento a tutto questo: CAOS !!!

In aggiunta a quanto detto dobbiamo considerare che diversi linguaggi di programmazione popolari come Java e C# non supportano l’ereditarietà multipla, il che limita i vantaggi di questo approccio.

Il design pattern Decorator fornisce un’alternativa flessibile all’ereditarietà per estendere la funzionalità degli oggetti. Tale pattern consente di arricchire dinamicamente, a run-time, un oggetto con nuove funzionalità: è possibile impilare uno o più decorator uno sopra l’altro, ciascuno aggiungendo nuove funzionalità. Riportiamo quindi le differenze tra Decorator e ereditarietà evidenziando noti vantaggi del Decorator:

  • Un Decorator agisce a runtime a differenza dell’ereditarietà che estende con le sottoclassi il comportamento della classe padre in fase di compilazione.
  • Un Decorator può operare su qualsiasi implementazione di una determinata interfaccia, eliminando la necessità di creare sottoclassi di un’intera gerarchia di classi.
  • La sottoclasse aggiunge comportamento al momento della compilazione e la modifica interessa tutte le istanze della classe originale; il decorator pattern può fornire nuovi comportamenti in fase di esecuzione per i singoli oggetti.
  • L’uso del modello decorator porta a codice pulito e testabile. I servizi creati con l’ereditarietà non possono essere testati separatamente dalla sua classe padre perché non esiste un meccanismo per sostituire una classe padre con uno stub.

 

Il decorator pattern è quindi molto utile e utilizzato dai programmatori più esperti, i quali lo utilizzano al posto della ereditarietà in situazioni in cui è necessario aggiungere/modificare a runtime il comportamento di un oggetto senza scomodare l’ereditarietà. Un caso noto di utilizzo del decorator è quello di alcune classi del core di java

java.io.BufferedReader;
java.io.BufferedWriter; 
java.io.FileReader;
java.io.Reader;

Andiamo adesso a descrivere i componenti del Decorato Pattern e il loro funzionamento riportando il diagramma UML originale del GoF.

schema del pattern

La struttura del decorator pattern si compone di quattro elementi principali:

  • Component: rappresenta l’interfaccia dell’oggetto che dovrà essere
    decorato dinamicamente,
  • ConcreteComponent: rappresenta l’oggetto a cui andranno aggiunte
    le nuove funzionalità,
  • Decorator: rappresenta l’interfaccia tra il Component e i ConcreteDecorator,
    possiede un riferimento al Component e un’interfaccia a esso conforme,
  • ConcreteDecorator: rappresentano gli oggetti che aggiungono le
    nuove funzionalità ai ConcreteComponent.

Passiamo adesso alla pratica con un esempio di utilizzo del Decorator in Java

ESEMPIO – paninoteca rossi

L’esempio funzionante che vi sto per mostrare lo potete scaricare al seguente LINK clonando il repo.

Il dominio applicativo di questo esempio è quello della paninoteca Rossi che vende diversi tipi di panini, i quali sono identificati da un nome e un prezzo. Nel nostro esempio prevediamo due tipologie di panino: Hamburger e CheeseBurger. Entrambe le tipologie di Panino non hanno al loro interno la Maionese e il Ketchup; se il cliente vuole aggiungere uno o più ingredienti extra deve pagare una somma aggiuntiva. Il nostro esercizio è quello di stampare a video per ogni panino il nome e il relativo prezzo (considerando in caso di aggiunta di ingredienti extra anche quest’ultimi nel prezzo totale). Senza l’uso del decorator, anche un dominio applicativo così semplice e piccolo, comporterebbe una gerarchia di classi medio-grande; se volessimo modellare l’aggiunta di un ingrediente con l’ereditarietà avremmo le seguenti Classi:

  • Hamburger (classe per modellare un Hamburger)
    • HamburgerMaionese (sotto classe di Hamburger per modellare un Hamburger su cui è stato aggiunto l’ingrediente extra Maionese)
    • HamburgerKetchup (sotto classe di Hamburger per modellare un Hamburger su cui è stato aggiunto l’ingrediente extra Ketchup)
    • HamburgerKetchupMaionese (sotto classe di Hamburger per modellare un Hamburger su cui è stato aggiunto l’ingrediente extra Ketchup e quello Maionese)
  • CheeseBurger (classe per modellare un CheeseBurger)
    • CheeseBurgerMaionese (sotto classe di CheeseBurger per modellare un Hamburger su cui è stato aggiunto l’ingrediente extra Maionese)
    • CheeseBurgerKetchup (sotto classe di CheeseBurger per modellare un Hamburger su cui è stato aggiunto l’ingrediente extra Ketchup)
    • CheeseBurgerKetchupMaionese (sotto classe di CheeseBurger per modellare un Hamburger su cui è stato aggiunto l’ingrediente extra Ketchup e quello Maionese)

9 classi per modellare un dominio così semplice! E se il numero delle tipologie di ingredienti aggiuntivi crescesse? La gerarchie delle classi esploderebbe (Exploding class hierarchy).

Risolviamo questo problema con il decorator pattern:

Component: Prima di tutto abbiamo bisogno di una prima classe astratta da cui deriveranno tutti i prodotti della nostra paninoteca: la classe Consumation modellerà la generica consumazione del cliente alla paninoteca Rossi, la quale sarà identificato da un nome del prodotto venduto e prezzo.

public abstract class Consumation {

 String productName = "";

 public String getProductName() {
 return productName;
 }

 public abstract double getPrice();
}


Decorator: Abbiamo poi bisogno della classa ExtraAdditionDecorator che modellerà ogni possibile aggiunta non prevista ad un prodotto: ad esempio un ingrediente aggiuntivo in un panino. Tale classe fa da classe base per i Decorator, ovvero modella tutti gli ingredienti aggiuntivi.

public abstract class ExtraAdditionDecorator extends Consumation {
 protected Consumation consumation;

 @Override
 public abstract String getProductName();

}

ConcreteComponent: Andiamo adesso a definire un po’ di entità del nostro dominio; ovvero dei prodotti venduti alla paninoteca Rossi. Nel nostro esempio vengono vendute due tipologie di panino:

public class CheeseBurger extends Consumation {
 public CheeseBurger() {
 productName = "CheeseBurger";
 }
 @Override
 public double getPrice() {
 return 2.50;
 }
}
public class Hamburger extends Consumation {
    public Hamburger() {
        productName = "Hamburger";
    }
    @Override
    public double getPrice() {
        return 2.00;
    }
}

ConcreteDecorator: Infine andiamo a definire un paio di decorator concreti; nel nostro esempio due tipi di ingredienti non previsti e compresi nei prezzi del menu: ExtraKetchupDecorator e ExtraMaioneseDecorator

public class ExtraMaioneseDecorator extends ExtraAdditionDecorator {


    public ExtraMaioneseDecorator(Consumation consumation){
        this.consumation = consumation;
    }

    @Override
    public String getProductName() {
        return consumation.getProductName()+ " con extra maionese";
    }

    @Override
    public double getPrice() {
        return consumation.getPrice()+0.20;
    }
}
public class ExtraKetchupDecorator extends ExtraAdditionDecorator {


    public ExtraKetchupDecorator(Consumation consumation){
        this.consumation = consumation;
    }

    @Override
    public String getProductName() {
        return consumation.getProductName()+ " con extra ketchup";
    }

    @Override
    public double getPrice() {
        return consumation.getPrice()+0.10;
    }
}

MAIN:Riportiamo adesso un semplice main:

public class Main {

 public static void main(String[] args) {
 //Hamburger
 Consumation hamburger = new Hamburger();
 System.out.println("Prodotto:" +
 hamburger.productName +
 " di prezzo " + String.format("%.2f", hamburger.getPrice()
 ));

 Consumation cheeseburger = new CheeseBurger();

 //voglio aggiungere la maionese al burger
 Consumation hamburgerConMaionese = new ExtraMaioneseDecorator(hamburger);
 System.out.println("Prodotto:" +
 hamburgerConMaionese.getProductName() +
 " di prezzo " + String.format("%.2f", hamburgerConMaionese.getPrice()));
 //voglio aggiungere la maionese e il ketchup al burger
 Consumation hamburgerConMaioneseKetchup = new ExtraKetchupDecorator(new ExtraMaioneseDecorator(hamburger));
 System.out.println("Prodotto:" +
 hamburgerConMaioneseKetchup.getProductName() +
 " di prezzo " + String.format("%.2f", hamburgerConMaioneseKetchup.getPrice()));

 Consumation cheeseburgerConMaionese = new ExtraMaioneseDecorator(cheeseburger);
 System.out.println("Prodotto:" +
 cheeseburgerConMaionese.getProductName() +
 " di prezzo " + String.format("%.2f", cheeseburgerConMaionese.getPrice()));

 }
}

Come mostra il codice precedente, all’interno del metodo main vengono create a runtime due istanze di Hamburger e CheeseBurger ma anche tre istanze di panino non definite come componenti concrete del dominio ma create grazie all’uso di decorator:

  • hamburgerConMaionese
  • hamburgerConMaioneseKetchup
  • cheeseburgerConMaionese

Per ognuno di questi panini andremo a visualizzare a video il nome del prodotto (comprensivo di ingrediente extra se utilizzato) e il prezzo.

Ecco l’output del main

Prodotto:Hamburger di prezzo 2,00
Prodotto:Hamburger con extra maionese di prezzo 2,20
Prodotto:Hamburger con extra maionese con extra ketchup di prezzo 2,30
Prodotto:CheeseBurger con extra maionese di prezzo 2,70

Come possiamo notare a video è stato stampato correttamente il nome del prodotto e il prezzo considerando anche gli ingredienti extra; il tutto utilizzando il decorator pattern in alternativa alla ereditarietà.

CONCLUSIONI

Abbiamo mostrato le potenzialità di questo pattern; ci tengo a precisare che oltre a semplificare la gerarchia delle classi e migliorare la testabilità, il decorator pattern incoraggia gli sviluppatori a scrivere codice che rispetta i principi di progettazione SOLID. Infatti il decorator pattern è spesso utile per aderire al (Principio di singola responsabilità) , in quanto consente di dividere la funzionalità tra classi con aree di interesse uniche e distinte aggiungendo nuove funzionalità ai nuovi oggetti senza modificare le classi esistenti (Open-Closed Principle). Qualcuno penserà: è il Santo Graal dei pattern? La risposta è no, come ogni volta rispondo ai commenti sui design pattern. Non esiste l’anti pattern e il Santo Graal dei pattern. Ogni pattern ha il suo caso di utilizzo dove l’utilizzo semplifica la vita e la qualità del codice ma in caso di abuso e uso in situazioni per cui non è stato progettato, diventa una vera e propria complicazione. Ha quindi anche il decorator dei potenziali svantaggi:

  • Un inconveniente noto di questo modello è che tutti i metodi nell’interfaccia decorator devono essere implementati nella classe decorator concreta. In effetti, i metodi che non aggiungono alcun comportamento aggiuntivo devono essere implementati come metodi di inoltro per mantenere il comportamento esistente. Al contrario, l’ereditarietà richiede solo sottoclassi per implementare metodi che modificano o estendono il comportamento della classe base.
  • I decorator possono complicare il processo di creazione dell’istanza del componente perché non solo devi istanziare il componente ma avvolgerlo anche in un certo numero di decorator.
  • Può essere complicato avere decorator che tengono traccia di altri decorator perché guardare indietro in più strati della catena dei decorator inizia a spingere il modello decorator oltre il suo vero intento.

Consiglio quindi di utilizzare il decorator nelle seguenti situazioni:

  • Quando le responsabilità e i comportamenti degli oggetti dovrebbero essere dinamicamente modificabili.
  • Le implementazioni concrete dovrebbero essere disaccoppiate da responsabilità e comportamenti.

Come menzionato più volte all’interno dell’articolo, tutto questo può essere sviluppato anche tramite sottoclassi; ma quando la gerarchia cresce a dismisura la situazione diventa ingestibile: man mano che aggiungi più comportamenti a una classe base, ti troverai presto a gestire l’incubo della manutenzione, poiché viene creata una nuova classe per ogni combinazione possibile. In questi casi il decorator pattern fornisce un’alternativa migliore a troppe sottoclassi e alla temuta Exploding class hierarchy.

bibliografia

  • Design Patterns: Elements of Reusable Object-Oriented Software (Autori: Erich Gamma, John Vlissides, Ralph Johnson, Richard Helm)
  • is-inheritance-dead 
  • https://neillmorgan.wordpress.com/2010/02/07/decorator-pattern-pros-and-cons/

 

A proposito di me

Dario Frongillo

Uno degli admin di Italiancoders e dell iniziativa devtalks.
Dario Frongillo è un software engineer e architect, specializzato in Web API, Middleware e Backend in ambito cloud native. Attualmente lavora presso NTT DATA, realtà di consulenza internazionale.
E' membro e contributor in diverse community italiane per developers; Nel 2017 fonda italiancoders.it, una community di blogger italiani che divulga articoli, video e contenuti per developers.

Di Dario Frongillo

Gli articoli più letti

Articoli recenti

Commenti recenti