Modèle de conception de stratégie dans Java 8

1. Introduction

Dans cet article, nous verrons comment nous pouvons implémenter le modèle de conception de stratégie dans Java 8.

Tout d'abord, nous donnerons un aperçu du modèle et expliquerons comment il était traditionnellement implémenté dans les anciennes versions de Java.

Ensuite, nous réessayerons le modèle, uniquement cette fois avec Java 8 lambdas, réduisant la verbosité de notre code.

2. Modèle de stratégie

Essentiellement, le modèle de stratégie nous permet de modifier le comportement d'un algorithme lors de l'exécution.

En règle générale, nous commençons avec une interface qui est utilisée pour appliquer un algorithme, puis nous l'implémentons plusieurs fois pour chaque algorithme possible.

Supposons que nous ayons l'obligation d'appliquer différents types de remises à un achat, selon qu'il s'agisse de Noël, de Pâques ou du nouvel an. Tout d'abord, créons une interface Discounter qui sera implémentée par chacune de nos stratégies:

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

Alors disons que nous voulons appliquer une réduction de 50% à Pâques et une réduction de 10% à Noël. Implémentons notre interface pour chacune de ces stratégies:

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)); } } 

Enfin, essayons une stratégie dans un test:

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

Cela fonctionne assez bien, mais le problème est que cela peut être un peu pénible d'avoir à créer une classe concrète pour chaque stratégie. L'alternative serait d'utiliser des types internes anonymes, mais cela reste assez détaillé et pas beaucoup plus pratique que la solution précédente:

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

3. Tirer parti de Java 8

Depuis la sortie de Java 8, l'introduction de lambdas a rendu les types internes anonymes plus ou moins redondants. Cela signifie que la création de stratégies en ligne est maintenant beaucoup plus propre et plus facile.

De plus, le style déclaratif de la programmation fonctionnelle nous permet d'implémenter des modèles qui n'étaient pas possibles auparavant.

3.1. Réduire la verbosité du code

Essayons de créer un EasterDiscounter en ligne , mais cette fois en utilisant une expression lambda:

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

Comme nous pouvons le voir, notre code est maintenant beaucoup plus propre et plus maintenable, réalisant la même chose qu'avant mais en une seule ligne. Essentiellement, un lambda peut être considéré comme un remplacement d'un type interne anonyme .

Cet avantage devient plus évident lorsque nous voulons déclarer encore plus de Discounters en ligne:

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

Lorsque nous voulons définir de nombreux Discounters, nous pouvons tous les déclarer statiquement au même endroit. Java 8 nous permet même de définir des méthodes statiques dans les interfaces si nous le voulons.

Donc, au lieu de choisir entre des classes concrètes ou des types internes anonymes, essayons de créer des lambdas dans une seule 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)); } } 

Comme nous pouvons le voir, nous réalisons beaucoup de choses avec un code très limité.

3.2. Tirer parti de la composition des fonctions

Modifions notre interface Discounter pour qu'elle étende l' interface UnaryOperator , puis ajoutons une méthode combine () :

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

Essentiellement, nous refactorisons notre Discounter et tirons parti du fait que l'application d'une remise est une fonction qui convertit une instance BigDecimal en une autre instance BigDecimal , ce qui nous permet d'accéder à des méthodes prédéfinies . Comme UnaryOperator est livré avec une méthode apply () , nous pouvons simplement remplacer applyDiscount par elle.

La méthode combine () est juste une abstraction autour de l'application d'un Discounter aux résultats de ceci. Il utilise la fonctionnalité intégrée apply () pour y parvenir.

Maintenant, essayons d'appliquer plusieurs Discounters cumulativement à un montant. Nous allons le faire en utilisant la fonctionnelle reduction () et notre combine ():

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

Portez une attention particulière au premier argument de réduction . Lorsqu'aucune remise n'est fournie, nous devons renvoyer la valeur inchangée. Ceci peut être réalisé en fournissant une fonction d'identité comme décompteur par défaut.

Il s'agit d'une alternative utile et moins verbeuse à l'exécution d'une itération standard. Si nous considérons les méthodes que nous sortons de la boîte pour la composition fonctionnelle, cela nous donne également beaucoup plus de fonctionnalités gratuitement.

4. Conclusion

Dans cet article, nous avons expliqué le modèle de stratégie et montré comment nous pouvons utiliser des expressions lambda pour l'implémenter d'une manière moins verbeuse.

L'implémentation de ces exemples est disponible à l'adresse over sur GitHub. Il s'agit d'un projet basé sur Maven, il devrait donc être facile à exécuter tel quel.