Design Pattern Strategy

Description

Le design pattern Strategy vise à créer une famille d'algorithmes associés (comportement) directement reliés à un contexte. Dans ce cas, chaque stratégie représente un métier / fonctionnalité qui représentera le contexte en fonction de la stratégie.

Mise en situation

🛒 Vous développez un e-commerce, qui dit e-commerce dit panier avec différents moyens de paiement. Aujourd'hui, il existe plusieurs moyens de paiement sur internet. Une solution serait de développer une structure conditionnelle pour adapter le choix de l'utilisateur. Le souci dans ce cas est que le code peut vite devenir illisible, voire impossible à maintenir :

  • Vous changez une ligne de code, cela peut avoir un impact sur le comportement global.
  • Un bug se présente, vous devez trouver le problème et passer énormément de temps à le résoudre.
  • Vous perdez du temps à vous assurer que votre algorithme fonctionne pour chacun des moyens de paiement.

C'est là que le pattern Strategy vient à la rescousse :

  • Chaque moyen de paiement est une stratégie (classe séparée) qui implémente son algorithme.
  • Le contexte de paiement s'adaptera à la stratégie choisie par l'utilisateur.

Ainsi, nous réglons les problèmes d'illisibilité et de maintenabilité.

Avantages et inconvénients

Avantages Inconvénients
✔️Meilleure structuration du code grâce à la décomposition des classes permettant des segments plus fins. Ainsi si l'on doit modifier des stratégies, on peut alors modifier une classe sans toucher au contexte. ❌Frais généraux (surdimensionnement) dans le cas où le contexte implémente crée et initialise des paramètres qui ne seront jamais utilisés par celle-ci. Ce cas peut être évité en augmentant le couplage entre les stratégies et contextes.
✔️Maintenabilité facilitée: chaque classe à un métier précis. Dans le cas d'utilisation de sous-classe via l'héritage, on mélangera l'implémentation de l'algorithme avec celle du contexte, ce qui deviendra vite pénible à maintenir. Ainsi ajouter, des fonctionnalités à une stratégie ou résoudre un bug, devient beaucoup plus simple et moins dangereux dû au couplage faible. ❌Peut créer des incompréhensions dans le cas d'une mauvaise implémentation, le choix d'une stratégie peut devenir complexe, voir incompréhensible. Cela peut conduire à des problèmes de mise en oeuvre. Il faut donc l'utiliser uniquement lorsque l'on peut séparer des comportements distincts et pertinents.
✔️Élimination des structures conditionnelles par la création de plusieurs classes qui représente un contexte. Dans le cas contraire, on aurait un contexte avec plusieurs structures contionnelles pour déte.rminer le bon algorithme. ❌Augmentation du nombre d'objets créés, étant donné que chaque classe représente une stratégie, on instancie alors beaucoup d'objets. Il est possible de contrer ce problème en couplant le design pattern flyweight avec ce design pattern pour créer des objets sans état que les contextes partageront.

Quand l'utiliser ?

  • Vous avez plusieurs algorithmes/comportement dans un objet à implémenter.
  • Vous avez plusieurs classes dont la seule différence est le comportement à exécuter.
  • Vous savez que le comportement d’une méthode peut évoluer vers d’autres comportements.
  • Vous avez énormément de structure conditionnelle.
  • Vous voulez avez une logique métier qui dans le cas du contexte n’a pas besoin d’être implémenté dans cette classe.

Diagramme UML

Dans le cas de cet exemple, nous développons un système de paiement. Ici, nous savons que le site possède deux moyens de paiement :

  • Via votre carte de crédit
  • Via paypal

Face à cette situation nous remarquons qu'un pattern strategy peut être ajouté.

Mais pourquoi ?

Imaginons qu’un nouveau moyen de paiement sera ajouté dans le futur, il nous suffira ici d’ajouter une nouvelle classe définissant notre stratégie sans impacter le reste du code. Nous gagnons donc en flexibilité, mais aussi en maintenabilité.

Dans le cas de cet exemple, notre contexte sera la classe shoppingCart. En effet, cette classe possède une fonction pay() qui prend en paramètre l'interface Payment. Dans le cas où nous n'utiliserions pas ce pattern, nous serions confrontés à utiliser des structures de contrôle ce qui deviendra très vite illisible en cas de nombreux moyen de paiement.

Implémentation

Commençons par créer la base de notre design package : l'interface Payment.

Payment

 package com.strategy.payment.algorithm;

public interface Payment {
   public void pay(int amount);
}

Comme nous pouvons le voir jusqu’ici il n'y a rien de compliqué. On déclare la méthode pay qui sera implémentée dans les diverses stratégies. Pour varier le comportement de cette méthode, nous allons créer plusieurs stratégies. Dans cette méthode, et pour les différentes stratégies, nous afficherons un message dans la console afin de distinguer quelle stratégie est utilisée.

CreditCardAlgorithm

 package com.strategy.payment.algorithm;

public class CreditCardAlgorithm implements Payment {

    private String name;
    private String cardNumber;
    private String card;
    private String expirationDate;

    public CreditCardAlgorithm(String name, String cardNumber) {
        this.name = name;
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + " paid with CreditCard");
    }

}

PaypalAlgorithm

 package com.strategy.payment.algorithm;

public class PaypalAlgorithm implements Payment {

    private String email;
    private String password;

    public PaypalAlgorithm(String email, String password) {
        this.email = email;
        this.password = password;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + " paid with Paypal");
    }
}

Nos deux algorithmes sont maintenant définis ! Vous l’aurez compris, ici ils sont très simplifiés afin de rendre l’exemple plus compréhensible. Maintenant que nous avons nos stratégies, créons notre contexte. Ce contexte étant un “panier d’achat”, il est important de définir un modèle pour nos produits :

Product

 package com.strategy.payment.model;

public class Product {
    private int price;

    public Product(int price) {
        this.price = price;
    }

    public int getPrice() {
        return this.price;
    }
}

Maintenant que notre modèle pour un produit est implémenté, nous avons tout ce dont nous avons besoin pour réaliser notre panier :

Shopping cart

 package com.strategy.payment.context;

import java.util.ArrayList;
import java.util.List;

import com.strategy.payment.algorithm.Payment;
import com.strategy.payment.model.Product;

public class ShoppingCart {

    List productList;

    public ShoppingCart() {
        this.productList = new  ArrayList<>();
    }

    public void addProduct(Product product) {
        productList.add(product);
    }

    public void removeProduct(Product product) {
        productList.remove(product);
    }

    public int calculateTotal() {
        int sum = 0;
        for (Product product : productList) {
            sum += product.getPrice();
        }
        return sum;
    }

    public void pay(Payment paymentStrategy) {
        int amount = calculateTotal();
        paymentStrategy.pay(amount);
    }
}

Notre context est défini ainsi que nos stratégies, maintenant nous allons tester cela directement dans la méthode main:

Main

 package com.strategy.payment;

import com.strategy.payment.algorithm.CreditCardAlgorithm;
import com.strategy.payment.algorithm.PaypalAlgorithm;
import com.strategy.payment.context.ShoppingCart;
import com.strategy.payment.model.Product;

public class Main {
    public static void main(String[] args) {

        ShoppingCart cart = new ShoppingCart();

        Product pants = new Product(25);
        Product shirt = new Product(15);

        cart.addProduct(pants);
        cart.addProduct(shirt);

        // Payment decisions
        cart.pay(new PaypalAlgorithm("email", "12234"));

        cart.pay(new CreditCardAlgorithm("Me", "BE844654454545"));

    }
}

Resultat :

 40 paid with Paypal
40 paid with CreditCard

Ici, nous avons donc fini notre implémentation. Ce que nous pouvons remarquer :

  • Dans le cas où je voudrais modifier le comportement de la fonction pay, je ne perturberai pas le comportement d'une autre stratégie.
  • Dans le cas où un nouveau mode de paiement devrait être implémenté, il suffira d'ajouter une nouvelle classe.
  • Si un bug survient, il sera plus facile d'investiguer dans la stratégie ciblée.
  • Ici, nous n'avons aucune structure conditionnelle. Cela veut dire qu'un client qui sélectionne un moyen de paiement sélectionne l'objet de la stratégie ciblée.
  • Les algorithmes sont bien scindés, dans le cas où l'on aurait trop de classes un pattern peut venir renforcer ce pattern (Fly weight).

Developpeur et architecte passionné, qui souhaite partagé son univers et ses découvertes afin de rendre les choses plus simple pour chacun