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

1. Introduction

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

Dans un premier temps, nous allons donner un aperçu du modèle et expliquer comment il est traditionnellement implémenté dans les anciennes versions de Java.

Nous essaierons ensuite le modèle, mais cette fois avec Java 8 lambdas, ce qui réduira la verbosité de notre code.

2. Modèle de stratégie

  • Le modèle de stratégie nous permet essentiellement de modifier le comportement d’un algorithme lors de l’exécution. **

En règle générale, nous commençons par 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, que ce soit pour Noël, Pâques ou le Nouvel An.

Commençons par créer une interface Discounter qui sera implémentée par chacune de nos stratégies:

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

Ensuite, disons que nous souhaitons appliquer une réduction de 50% à Pâques et 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 qu’il peut être un peu pénible de devoir créer une classe concrète pour chaque stratégie. L’autre solution consisterait à utiliser des types internes anonymes, mais c’est toujours très bavard 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 désormais beaucoup plus simple et plus propre.

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 inline, cette fois-ci en utilisant une expression lambda:

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

Comme nous pouvons le constater, notre code est maintenant beaucoup plus propre et plus facile à gérer. Essentiellement, un lambda peut être considéré comme un substitut à un type interne anonyme .

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

List<Discounter> 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 un grand nombre de Discounters, nous pouvons les déclarer de manière statique en un seul endroit. Java 8 nous permet même de définir des méthodes statiques dans les interfaces si nous le souhaitons.

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 constater, nous réalisons beaucoup dans un code pas très important.

3.2. Composition de la fonction de levier

Modifions notre interface Discounter de manière à étendre l’interface UnaryOperator , puis ajoutons une méthode combine () :

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

Nous refactorisons essentiellement notre Discounter et tirons parti du fait qu’appliquer une remise est une fonction qui convertit une instance de BigDecimal en une autre instance de BigDecimal _, nous permettant d’accéder à des méthodes prédéfinies . nous pouvons simplement remplacer applyDiscount_ par elle.

La méthode combine () est simplement une abstraction qui consiste à appliquer un Discounter aux résultats de this. . Elle utilise la fonction apply () intégrée pour y parvenir.

Maintenant, essayons d’appliquer plusieurs Discounters cumulativement à un montant. Nous ferons cela en utilisant les reduce () fonctionnels et notre combine ():

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

combinedDiscounter.apply(...);

Portez une attention particulière au premier argument reduce . Lorsqu’aucun rabais n’est fourni, nous devons renvoyer la valeur inchangée. Ceci peut être réalisé en fournissant une fonction d’identité en tant que décompteur par défaut.

C’est une alternative utile et moins verbeuse que d’effectuer une itération standard. Si nous considérons les méthodes de composition fonctionnelle que nous obtenons, 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 utiliser les expressions lambda pour le mettre en œuvre de manière moins verbeuse.

La mise en œuvre de ces exemples est disponible sur over sur GitHub . Ceci est un projet basé sur Maven, il devrait donc être facile à exécuter tel quel.