O — Ouvert / Fermé : les frais de livraison
Remplacer le switch des transporteurs (Colissimo, Chronopost, retrait magasin) par le pattern Strategy et ajouter le point relais sans toucher au code existant.
Objectif de la leçon
Remplacer un switch sur les modes de livraison par le pattern Strategy, pour ajouter un transporteur sans toucher au code existant.
La règle
Le code doit être ouvert à l’extension et fermé à la modification. On ajoute un comportement en ajoutant une classe, pas en modifiant une classe qui fonctionne déjà en production.
L’analogie : une multiprise. Pour brancher un nouvel appareil, vous ne recâblez pas le mur, vous ajoutez une prise.
Sans pattern : le switch des frais de livraison
public class CalculateurLivraison {
public double frais(String mode, Panier panier) {
switch (mode) {
case "COLISSIMO":
return panier.totalHT() >= 50 ? 0.0 : 4.90;
case "CHRONOPOST":
return 9.90;
case "RETRAIT_MAGASIN":
return 0.0;
default:
throw new IllegalArgumentException("Mode inconnu : " + mode);
}
}
}
Le problème
Octobre : « Pour le Black Friday, on ajoute le point relais à 3,50 € ». Novembre : « la livraison express le jour même ». Janvier : « la livraison internationale ».
- Chaque nouveau transporteur oblige à rouvrir
CalculateurLivraison, testé et en production. - Le même
switchexiste souvent ailleurs : délai de livraison, libellé sur la page panier, choix de l’étiquette. On en oublie un, et c’est l’incident en pleine campagne. - Le test unitaire grossit et devient illisible.
Avec pattern : Strategy
Pattern utilisé : Strategy. Chaque mode de livraison devient une classe qui respecte un contrat commun, et regroupe tout ce qui le concerne (frais, délai, libellé).
// Le contrat
public interface ModeLivraison {
String code();
String libelle();
double frais(Panier panier);
int delaiJours();
}
// Une classe par transporteur
@Component
public class Colissimo implements ModeLivraison {
private static final double SEUIL_GRATUIT = 50.0;
public String code() { return "COLISSIMO"; }
public String libelle() { return "Colissimo à domicile"; }
public double frais(Panier p) { return p.totalHT() >= SEUIL_GRATUIT ? 0.0 : 4.90; }
public int delaiJours() { return 3; }
}
@Component
public class Chronopost implements ModeLivraison {
public String code() { return "CHRONOPOST"; }
public String libelle() { return "Chronopost express"; }
public double frais(Panier p) { return 9.90; }
public int delaiJours() { return 1; }
}
@Component
public class RetraitMagasin implements ModeLivraison {
public String code() { return "RETRAIT_MAGASIN"; }
public String libelle() { return "Retrait en magasin"; }
public double frais(Panier p) { return 0.0; }
public int delaiJours() { return 2; }
}
// Le calculateur ne changera plus jamais
@Service
public class CalculateurLivraison {
private final Map<String, ModeLivraison> modes;
// Spring injecte automatiquement toutes les implémentations
public CalculateurLivraison(List<ModeLivraison> modes) {
this.modes = modes.stream()
.collect(Collectors.toMap(ModeLivraison::code, m -> m));
}
public double frais(String code, Panier panier) {
ModeLivraison mode = modes.get(code);
if (mode == null) throw new IllegalArgumentException("Mode inconnu : " + code);
return mode.frais(panier);
}
public Collection<ModeLivraison> disponibles() { return modes.values(); }
}
Le point relais arrive ? On ajoute un fichier, et c’est tout. Il apparaît automatiquement dans la page panier :
@Component
public class PointRelais implements ModeLivraison {
public String code() { return "POINT_RELAIS"; }
public String libelle() { return "Point relais"; }
public double frais(Panier p) { return 3.50; }
public int delaiJours() { return 4; }
}
Le schéma : avant / après
Comment lire le schéma
- Avant : le nouveau besoin (en jaune) oblige à rouvrir la classe rouge et à ajouter une branche au
switch. - Après :
CalculateurLivraisonne connaît que l’interfaceModeLivraison. Il ne change plus. - Chaque transporteur implémente l’interface.
PointRelais(en jaune) est ajouté à côté des autres, sans toucher à l’existant. - Légende : flèche pleine = « utilise » · flèche à triangle blanc = « implémente / hérite » · cadre pointillé jaune = interface · vert = conforme · rouge = problème.
Astuce d’expert
Java 21 offre une alternative quand les variantes sont fermées et connues à la compilation : une sealed interface + switch avec pattern matching. Le compilateur signale tout cas oublié (switch exhaustif, sans default). Gardez Strategy + injection Spring quand de nouvelles variantes arrivent souvent (transporteurs, promos) ; préférez le sealed pour un domaine stable (statuts de commande, par exemple).
Ce qu’on gagne
| Critère | Sans pattern | Avec pattern |
|---|---|---|
| Ajouter un transporteur | Modifier une classe en production | Ajouter une classe |
| Risque de régression | Tous les modes existants | Zéro sur l’existant |
| Tests | Un gros test qui grossit | Un petit test par transporteur |
| Réutilisation | Le switch recopié (frais, délai, libellé) | Un seul objet sert au panier, à la commande et à l’étiquette |
| Recette avant le Black Friday | Tout re-tester | Tester seulement la nouvelle classe |
Bonnes pratiques
- Symptôme : un
switchou une cascade deifsur un type (transporteur, promo, moyen de paiement) qui grossit à chaque campagne. - Regroupez dans la stratégie tout ce qui dépend du type : prix, délai, libellé.
- Avec Spring, injectez une
List<Interface>: chaque nouvelle classe@Componentest découverte automatiquement.
Erreurs fréquentes
- Créer une Strategy pour deux cas qui ne bougeront jamais : un
ifsuffit. - Laisser un
switchdans un autre service « juste pour le libellé » : le principe O est cassé ailleurs.
Lab · 30 min · Les codes promo
Le service suivant gère les codes promo avec un switch :
public double appliquerPromo(String type, double valeur, Panier panier) {
switch (type) {
case "POURCENTAGE": return panier.totalHT() * (1 - valeur / 100);
case "MONTANT_FIXE": return Math.max(0, panier.totalHT() - valeur);
default: return panier.totalHT();
}
}
- Créez une interface
Promotionet une classe par type. - Le marketing ajoute « 2 achetés, le 3e offert » : implémentez-le sans modifier les classes existantes.
- Écrivez un test par promotion.
À retenir
- Nouveau besoin = nouvelle classe, pas nouvelle branche de
switch. - Strategy = une interface + une classe par variante.
- Ne pas l’appliquer quand il n’y a pas de variation réelle.
Quiz rapide
- Quel pattern remplace un
switchsur le transporteur ? → Strategy. - Combien de fichiers existants modifiez-vous pour ajouter le point relais ? → Zéro.
- Comment Spring trouve-t-il les transporteurs ? → En injectant
List<ModeLivraison>.
Suivez votre progression. Créez un compte gratuit pour cocher vos leçons et reprendre là où vous en étiez.
Créer un compte gratuit