mer 09 ottobre 2024 - Lo Sviluppatore  anno VI

Java 8: Pattern Decorator in salsa Lambda

Pattern Decorator
Condividi

Con l’avvento di java 8, e quindi dei nuovi strumenti che offre questa versione del linguaggio come le espressioni lambda, gli stream, i method reference e la programmazione funzionale in genere, è possibile scrivere in generale codice migliore. Questo discorso possiamo applicarlo anche ai famosi Design Pattern della GoF che si prestano alla riscrittura mediante l’uso della programmazione funzionale. In particolare in questo articolo vedremo il Pattern Decorator riscrivendolo con l’uso delle espressioni lambda.

Ricordiamo che la motivazione per l’introduzione delle espressioni lambda in Java è stato quello di soddisfare un pattern architetturale detto “comportamento parametrizzato” cioè di poter passare ad esempio come argomento di un metodo una funzione, appunto un “comportamento”. Questo pattern consente di far fronte ai cambiamenti dei requisiti, consentendo di scrivere codice più flessibile. Prima di Java 8,  realizzare questo pattern era più complicato ma a desso l’applicazione di tale pattern con l’uso della lambda è decisamente più conciso.

Il Pattern Decorator (versione classica)

Questo pattern viene utilizzato quando è necessario modificare il comportamento di un oggetto a runtime senza di controparte modificare il comportamento degli altri oggetti dello stesso tipo.  Il pattern viene implementato “avvolgendo” la classe originale, tipicamente passata come argomento nel costruttore,  in un’altra classe, appunto il decorator.  Prima di vedere come implementare il pattern in Java 8 proponiamo la versione classica. Per fare questo utilizziamo un esempio comune, quello della preparazione di una pizza con vari condimenti.

Partiamo dall’interfaccia che definisce il nostro oggetto:

public interface Pizza {
    String infornaPizza();
}

Definiamo un oggetto concreto:

public class PizzaBase implements Pizza {
    @Override
    public String infornaPizza() {
        return "Pizza Base";
    }
}

E passiamo a “decorare” questo oggetto per poter comporre un oggetto arricchito, in questo caso una pizza con vari condimenti.
Partiamo dal decoratore astratto per poi realizzare due decorator concreti:

public abstract class PizzaDecorator implements Pizza {
    private final Pizza pizza;
 
    protected PizzaDecorator(Pizza pizza) {
        this.pizza = pizza;
    }
 
    @Override
    public String infornaPizza() {
        return pizza.infornaPizza();
    }
}
public class PizzaConFunghi extends PizzaDecorator {
    protected PizzaConFunghi (Pizza pizza) {
        super(pizza);
    }
 
    @Override
    public String infornaPizza() {
        return super.infornaPizza() + " con funghi";
    }
}
 
public class PizzaConProsciutto extends PizzaDecorator {
 
    protected ProsciuttoPizza(Pizza pizza) {
        super(pizza);
    }
 
    @Override
    public String infornaPizza() {
        return super.infornaPizza() + " con prosciutto";
    }
}

Per usare i decorator procediamo così:

Pizza pizza = new PizzaConFunghi(new PizzaBase());
String miaPizza= pizza.infornaPizza();   //Pizza base con funghi
 
pizza = new PizzaConFunghi(new PizzaConProsciutto(new PizzaBase()));
miaPizza  = pizza.infornaPizza();  //Pizza Base con prosciutto e funghi

 

Il Pattern Decorator (versione lambda)

Ora vediamo come implementare lo stesso pattern usando le lambda expression e vediamo come tale implementazione rende la cosa più concisa e ordinata.
L’approccio che si usa è quello di passare al decorator non più l’oggetto originale bensì la funzione che decorerà l’oggetto originale.

public class PizzaDecorator {
    private final Function<Pizza, Pizza> condimenti;
 
    private PizzaDecorator(Function<Pizza, Pizza>... condimentiDesiderati) {
        this.condimenti= Stream.of(condimentiDesiderati).reduce(Function.identity(), Function::andThen);
 
    }
 
    public static String infornaPizza(Pizza pizza, Function<Pizza, Pizza>... condimentiDesiderati) {
        return new PizzaDecorator(condimentiDesiderati).infornaPizza(pizza);
    }
 
private String infornaPizza(Pizza pizza) {
    return this.condimenti.apply(pizza).infornaPizza();
}
 
}

La seguente riga costruisce la catena di decorazioni da applicare:

        Stream.of(condimentiDesiderati).reduce(Function.identity(), Function::andThen);

Quì vediamo come vengono passati i decoratori (condimentiDesiderati) che sono delle funzioni e vengono concatenati col metodo andThen.

Questa riga si può anche scrivere in una maniera forse più chiara come:

(condimentoCorrente, condimentoSuccesivo) -> condimentoCorrente.andThen(condimentoSuccesivo)

E fa in modo che le funzioni siano chiamate in sequenza nell’ordine fornito.

Adesso possiamo definire dei decoratori concreti ad esempio aggiungendo dei metodi statici nella classe PizzaDecorator o anche nell’interfaccia:

public interface Pizza {
    String infornaPizza();
 
    static Pizza conFunghi(Pizza pizza) {
        return new Pizza() {
            @Override
            public String infornaPizza() {
                return pizza.infornaPizza() + " con funghi";
            }
        };
    }
 
    static Pizza conProsciuttoo(Pizza pizza) {
        return new Pizza() {
            @Override
            public String infornaPizza() {
                return pizza.infornaPizza() + " con prosciutto";
            }
        };
    }
}

E infine vediamo come usare i decoratori nel concreto:

String laMiaPizza = PizzaDecorator.infornaPizza(new BasicPizza(), Pizza::conFunghi, Pizza::conProsciutto);
 
//E ancora più semplicemente se si fa l'import statico di PizzaDecorator.infornaPizza:
String laMiaPizza = infornaPizza(new BasicPizza(), Pizza::withFunfghi, Pizza::conProsciutto);

Come si può vedere, il codice ottenuto è più chiaro e più conciso, e non abbiamo usato l’ereditarietà (in genere meccanismo meno flessibile e più verboso) per costruire i nostri decorator. Questo è solo uno dei tanti design pattern che possono essere migliorati utilizzando le lambda. Ci sono tanti altri casi in cui è possibile riscrivere in chiave funzionale sfruttando appieno le nuove caratteristiche che java 8 ci mette a disposizione.

Riferimenti

Decorator Design Pattern Using Lambdas

Lascia un commento

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

Top