
DESIGN PATTERNS IN JAVA | Decorator
Il pattern Decorator consente di aggiungere ad un oggetto nuove funzionalità a runtime senza modificare le altre istanze della classe.
CLASSIFICAZIONE
Il pattern Decorator è classificato tra i pattern strutturali.
PROBLEMA E CAMPO DI APPLICAZIONE
Quando occorre modificare una classe si ricorre all'ereditarietà che però applica le stesse modifiche a tutte le istanze della classe derivata.
Dovendo modificare una singola istanza, magari a runtime, la soluzione basata sull'ereditarietà si dimostra inefficace.
Inoltre ogni volta che occorre combinare due o più caratteristiche presenti in classi differenti bisogna creare una nuova sottoclasse e ben presto la gerarchia diventa ingestibile.
SOLUZIONE
Con il pattern Decorator si introduce una classe che aggiunge una nuova funzionalità all'oggetto originario.
L'oggetto "decorato" può essere a sua volta modificato con l'aggiunta di ulteriori funzionalità.
Non esiste un limite al numero di "decorazioni" applicabili.
Vediamo più in dettaglio quali sono i protagonisti del pattern:
- Component: un'interfaccia che viene implementata dagli oggetti che possono richiedere dinamicamente l'aggiunta di nuove funzionalità
- Concrete Component: una classe che implementa la precedente interfaccia
- Decorator: possiede un riferimento ad un oggetto Component e ne implementa la stessa interfaccia
- Concrete Decorator: estende le funzionalità di un Component modificandone lo stato (tramite nuovi attributi) o il comportamento (tramite nuovi metodi)
N.B. Premesso che sia le interfacce che le classi astratte non possono essere istanziate direttamente ci si può chiedere quale delle due tipologie sia più appropriata per definire il Component. Essenzialmente se è previsto del codice comune ha senso utilizzare una classe astratta in modo da implementare al suo interno i metodi già definiti, altrimenti conviene impiegare un'interfaccia delegando tutte le implementazioni alle classi concrete.
Passiamo ora ad un esempio concreto per chiarirci meglio le idee.
Supponiamo di voler gestire delle figure geometriche che possono essere "decorate" dinamicamente, modificando ad esempio il colore del bordo piuttosto che quello dello sfondo.
Partiamo con la creazione dell'interfaccia Shape
(corrispondente al Component del pattern)
public interface Shape {
void draw();
}
In questo caso è presente un solo metodo draw()
per disegnare la figura, ma in contesti più realistici ce ne saranno molti di più.
Quindi realizziamo la classe Circle
(ConcreteComponent del pattern) che implementa tale interfaccia e che fornisce un'implementazione del metodo draw()
che per semplicità stampa soltanto un semplice messaggio.
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Shape: Circle");
}
}
Possiamo creare tante classi quante sono le figure geometriche che intendiamo gestire.
E giungiamo all'elemento fondamentale del pattern, ovvero il Decorator, che ha le seguenti caratteristiche:
- implementa la stessa interfaccia di Component (ovvero
Shape
nel nostro caso) - mantiene un riferimento ad un oggetto Shape ovvero ad una classe concreta che implementa tale interfaccia
- si tratta di una classe astratta per consentire l'inizializzazione del riferimento all'oggetto Shape
- il metodo
draw()
richiama il suo omonimo dell'oggetto Shape referenziato
public abstract class ShapeDecorator implements Shape {
protected Shape shape;
public ShapeDecorator(Shape shape) {
this.shape = shape;
}
@Override
public void draw() {
shape.draw();
}
}
La classe astratta non può essere direttamente istanziata per cui occorre realizzare una classe concreta (ConcreteDecorator del pattern), ad esempio una classe che aggiunge un bordo bianco alla figura. In effetti è proprio all'interno di questo tipo di classi che viene "decorato" il metodo dell'istanza di Shape
che viene passata.
In questo specifico esempio andiamo ad intervenire su draw()
richiamando prima di tutto quello dell'istanza Shape
, visto che dobbiamo integrare e non sostituire del tutto la logica, e poi il metodo corrispondente alla nuova funzionalità che in questo caso è setWhiteBorder(shape)
.
public class WhiteBorderColorDecorator extends ShapeDecorator {
public WhiteBorderColorDecorator(Shape shape) {
super(shape);
}
@Override
public void draw() {
shape.draw();
setWhiteBorder(shape);
}
private void setWhiteBorder(Shape shape) {
System.out.println("Border color: White");
}
}
Aggiungiamo anche un altro ConcreteDecorator per aggiungere lo sfondo rosso alla figura.
Il codice ricalca in tutto e per tutto la struttura del precedente.
public class RedBackgroundColorDecorator extends ShapeDecorator {
public RedBackgroundColorDecorator(Shape shape) {
super(shape);
}
@Override
public void draw() {
shape.draw();
setRedBackground(shape);
}
private void setRedBackground(Shape shape) {
System.out.println("Background color: Red");
}
}
Siamo pronti per un test diretto.
In sostanza andiamo a creare una nuova figura geometrica, un Circle
, e la passiamo come parametro ad un ConcreteDecorator, ad esempio il RedBackgroundColorDecorator
che andremo ad istanziare a sua volta, e il tutto potrà essere ulteriormente "decorato" con il WhiteBorderColorDecorator
.
Il risultato di tutto questo "inscatolamento" sarà comunque un'istanza di Shape
grazie al fatto che tutti i componenti implementano la medesima interfaccia.
public class Client {
public static void main(String[] args) {
Shape shape = new WhiteBorderColorDecorator(new RedBackgroundColorDecorator(new Circle()));
shape.draw();
}
}
Eseguendo il codice si ottiene il seguente risultato
Shape: Circle
Background color: Red
Border color: White
Notiamo in particolare l'ordine in cui vengono eseguiti i vari metodi che compongono il draw()
definitivo.
[VIDEO]
[LINKS]
`