Modello di progettazione della strategia in Java 8

1. Introduzione

In questo articolo vedremo come implementare il modello di progettazione della strategia in Java 8.

Innanzitutto, forniremo una panoramica del modello e spiegheremo come è stato tradizionalmente implementato nelle versioni precedenti di Java.

Successivamente, proveremo di nuovo il pattern, solo questa volta con Java 8 lambda, riducendo la verbosità del nostro codice.

2. Schema strategico

In sostanza, il modello di strategia ci consente di modificare il comportamento di un algoritmo in fase di esecuzione.

In genere, si inizia con un'interfaccia che viene utilizzata per applicare un algoritmo e quindi lo si implementa più volte per ogni possibile algoritmo.

Supponiamo di avere l'obbligo di applicare diversi tipi di sconti a un acquisto, a seconda che si tratti di Natale, Pasqua o Capodanno. Per prima cosa, creiamo un'interfaccia di discount che verrà implementata da ciascuna delle nostre strategie:

public interface Discounter { BigDecimal applyDiscount(BigDecimal amount); } 

Allora diciamo di voler applicare uno sconto del 50% a Pasqua e uno sconto del 10% a Natale. Implementiamo la nostra interfaccia per ciascuna di queste strategie:

public static class EasterDiscounter implements Discounter { @Override public BigDecimal applyDiscount(final BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(0.5)); } } public static class ChristmasDiscounter implements Discounter { @Override public BigDecimal applyDiscount(final BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(0.9)); } } 

Infine, proviamo una strategia in un test:

Discounter easterDiscounter = new EasterDiscounter(); BigDecimal discountedValue = easterDiscounter .applyDiscount(BigDecimal.valueOf(100)); assertThat(discountedValue) .isEqualByComparingTo(BigDecimal.valueOf(50));

Funziona abbastanza bene, ma il problema è che può essere un po 'fastidioso dover creare una classe concreta per ogni strategia. L'alternativa sarebbe quella di utilizzare tipi interni anonimi, ma è ancora piuttosto prolisso e non molto più pratico della soluzione precedente:

Discounter easterDiscounter = new Discounter() { @Override public BigDecimal applyDiscount(final BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(0.5)); } }; 

3. Sfruttare Java 8

Da quando è stato rilasciato Java 8, l'introduzione di lambda ha reso i tipi interni anonimi più o meno ridondanti. Ciò significa che la creazione di strategie in linea è ora molto più semplice e pulita.

Inoltre, lo stile dichiarativo della programmazione funzionale ci consente di implementare modelli che prima non erano possibili.

3.1. Riduzione della verbosità del codice

Proviamo a creare un EasterDiscounter inline , solo questa volta utilizzando un'espressione lambda:

Discounter easterDiscounter = amount -> amount.multiply(BigDecimal.valueOf(0.5)); 

Come possiamo vedere, il nostro codice ora è molto più pulito e più manutenibile, ottenendo lo stesso di prima ma in una singola riga. In sostanza, un lambda può essere visto come un sostituto di un tipo interno anonimo .

Questo vantaggio diventa più evidente quando vogliamo dichiarare ancora più sconti in linea:

List discounters = newArrayList( amount -> amount.multiply(BigDecimal.valueOf(0.9)), amount -> amount.multiply(BigDecimal.valueOf(0.8)), amount -> amount.multiply(BigDecimal.valueOf(0.5)) );

Quando vogliamo definire molti discount, possiamo dichiararli staticamente tutti in un unico posto. Java 8 ci consente anche di definire metodi statici nelle interfacce, se lo desideriamo.

Quindi, invece di scegliere tra classi concrete o tipi interni anonimi, proviamo a creare lambda tutti in una singola classe:

public interface Discounter { BigDecimal applyDiscount(BigDecimal amount); static Discounter christmasDiscounter() { return amount -> amount.multiply(BigDecimal.valueOf(0.9)); } static Discounter newYearDiscounter() { return amount -> amount.multiply(BigDecimal.valueOf(0.8)); } static Discounter easterDiscounter() { return amount -> amount.multiply(BigDecimal.valueOf(0.5)); } } 

Come possiamo vedere, stiamo ottenendo molto in un codice non molto.

3.2. Sfruttare la composizione delle funzioni

Modifichiamo la nostra interfaccia di Discounter in modo che estenda l' interfaccia UnaryOperator , quindi aggiungiamo un metodo combination () :

public interface Discounter extends UnaryOperator { default Discounter combine(Discounter after) { return value -> after.apply(this.apply(value)); } }

Essenzialmente, stiamo rifattorizzando il nostro discount e sfruttando il fatto che l'applicazione di uno sconto è una funzione che converte un'istanza BigDecimal in un'altra istanza BigDecimal , permettendoci di accedere a metodi predefiniti . Poiché UnaryOperator viene fornito con un metodo apply () , possiamo semplicemente sostituire applyDiscount con esso.

Il metodo combination () è solo un'astrazione sull'applicazione di un discount ai risultati di questo. Utilizza la funzionalità integrata apply () per ottenere ciò.

Ora, proviamo ad applicare più sconti cumulativamente a un importo. Lo faremo utilizzando la funzione reduce () e la nostra combinazione ():

Discounter combinedDiscounter = discounters .stream() .reduce(v -> v, Discounter::combine); combinedDiscounter.apply(...);

Presta particolare attenzione al primo argomento di riduzione . Quando non sono previsti sconti, è necessario restituire il valore invariato. Ciò può essere ottenuto fornendo una funzione di identità come discount predefinito.

Questa è un'alternativa utile e meno prolissa all'esecuzione di un'iterazione standard. Se consideriamo i metodi che stiamo ottenendo fuori dagli schemi per la composizione funzionale, ci offre anche molte più funzionalità gratuitamente.

4. Conclusione

In questo articolo, abbiamo spiegato il modello di strategia e dimostrato anche come possiamo usare espressioni lambda per implementarlo in un modo meno dettagliato.

L'implementazione di questi esempi può essere trovata su GitHub. Questo è un progetto basato su Maven, quindi dovrebbe essere facile da eseguire così com'è.