Définition : Le modèle de stratégie définit une famille d'algorithmes, les encapsulate séparément, et permet de les remplacer les uns par les autres. Ce modèle permet de modifier les algorithmes sans impacter les clients qui les utilisent.
Cette définition peut sembler abstraite. Eaxminons un exemple concret pour mieux compréhension ce pattern.
Contexte : Un système de paiement dans un supermarché avec des promotions comme des remises ou des rabais conditionnels.
private double calculerTotal(String typePromo, String prix, String quantite){
double total;
switch (typePromo){
//Pas de remise
case "0":
total = Double.valueOf(prix) * Double.valueOf(quantite);
break;
//Remise 20%
case "1":
total = Double.valueOf(prix) * Double.valueOf(quantite) * 0.8;
break;
//Remise 30%
case "2":
total = Double.valueOf(prix) * Double.valueOf(quantite) * 0.7;
break;
default:
throw new IllegalStateException("Valeur inattendue: " + typePromo);
}
return total;
}
Ainsi, un système de tarification simple est mis en place. Cependant, en examinant le code, deux problèmes apparaissent. Premièrement, il y a trop de code redondant comme Double.valueOf. Deuxièmement, si une nouvelle exigence est ajoutée, comme "acheter 300, bénéficier de 20 de réduction", le code doit être modifié. Cela rappelle le pattern de factory simple. On pourrait créer une classe parente avec différentes méthodes de calcul comme sous-classes. Bien que le pattern factory simple résolve ce problème, que se passe-t-il si de nouvelles exigences apparaissent ? Par exemple, "acheter 500, bénéficier de 50". Dans ce cas, il faudrait ajouter une nouvelle sous-classe et modifier également la classe factory. Cette approche n'est pas idéale car la classe factory est exposée au client, et la modifier fréquemment n'est pas recommandé. C'est pourquoi le pattern de stratégie est préférable.
//Classe abstraite pour les algorithmes
public interface Strategie {
//Méthode algorithmique
public abstract void executerStrategie();
}
//Contient l'implémentation concrète de l'algorithme
public class StrategieConcreteA extends Strategie{
@Override
public void executerStrategie() {
System.out.println("Implémentation de la stratégie A");
}
}
public class StrategieConcreteB extends Strategie{
@Override
public void executerStrategie() {
System.out.println("Implémentation de la stratégie B");
}
}
Context est utilisé pour configurer une Strategie concrète et maintenir une référence vers un objet Strategie.
Strategie strategie;
//Constructeur pour initialiser avec un objet stratégie concret
public Contexte(Strategie strategie){
this.strategie = strategie;
}
//Appelle la méthode de l'algorithme selon la stratégie concrètes
public void executarOperation(){
strategie.executerStrategie();
}
Exemple d'utilisation :
public static void main(String[] args) {
Contexte contexte;
contexte = new Contexte(new StrategieConcreteA());
contexte.executarOperation();
contexte = new Contexte(new StrategieConcreteB());
contexte.executarOperation();
}
Voici la logique de base du pattern de stratégie. Appliquons-le maintenant au système de paiement du supermarché.
public abstract class Calculateur {
public abstract double calculer(double montant);
}
//Tarif normal
public class TarifStandard extends Calculateur {
@Override
public double calculer(double montant) {
return montant;
}
}
//Classe pour les remises
public class RemiseFixe extends Calculateur {
private double tauxRemise = 1d;
public RemiseFixe(String tauxRemise){
this.tauxRemise = Double.valueOf(tauxRemise);
}
@Override
public double calculer(double montant) {
return montant * tauxRemise;
}
}
//Classe pour les rabais conditionnels
public class RabaisConditionnel extends Calculateur {
private double seuilMinimum = 0.0d;
private double montantRabais = 0.0d;
public RabaisConditionnel(String seuilMinimum, String montantRabais){
this.seuilMinimum = Double.valueOf(seuilMinimum);
this.montantRabais = Double.valueOf(montantRabais);
}
@Override
public double calculer(double montant) {
double resultat = montant;
if (montant >= seuilMinimum){
return montant - Math.floor(montant / seuilMinimum) * montantRabais;
}
return resultat;
}
}
Gestionanire
public class GestionnairePaiement {
private Calculateur calculateur;
public GestionnairePaiement(Calculateur calc){
this.calculateur = calc;
}
public double obtenirResultat(double montant){
return calculateur.calculer(montant);
}
}
private double calculer(String choix, String prix, String quantite){
GestionnairePaiement gestionnaire;
switch (choix){
//Pas de remise
case "Tarif normal":
gestionnaire = new GestionnairePaiement(new TarifStandard());
break;
case "300-30":
gestionnaire = new GestionnairePaiement(new RabaisConditionnel("300", "30"));
break;
case "Remise 20%":
gestionnaire = new GestionnairePaiement(new RemiseFixe("0.8"));
break;
default:
throw new IllegalStateException("Valeur inattendue: " + choix);
}
double total = 0d;
total = gestionnaire.obtenirResultat(Double.valueOf(prix) * Double.valueOf(quantite));
return total;
}
Mais cette写法ne vous semble-t-elle pas étrange ? La création des classes est toujours faite côté client, ce qui est très similaire au pattern factory simple. Nous devons donc combiner le pattern de stratégie avec le pattern factory simple.
public class GestionnairePaiement {
private Calculateur calculateur;
public GestionnairePaiement(String type){
switch (type){
//Pas de remise
case "Tarif normal":
calculateur = new TarifStandard();
break;
case "300-30":
calculateur = new RabaisConditionnel("300", "30");
break;
case "Remise 20%":
calculateur = new RemiseFixe("0.8");
break;
default:
throw new IllegalStateException("Valeur inattendue: " + type);
}
}
public double obtenirResultat(double montant){
return calculateur.calculer(montant);
}
private double calculer(String choix, String prix, String quantite){
GestionnairePaiement gestionnaire;
gestionnaire = new GestionnairePaiement("300-30");//Sélection du type
double total = 0d;
total = gestionnaire.obtenirResultat(Double.valueOf(prix) * Double.valueOf(quantite));
return total;
}
Cette approche est bien meilleure que précédemment. Comparée au pattern factory, où le client doit connaître CashSuper et CashFactory, maintenant le client n'a qu'à connaître GestionnairePaiement, ce qui réduit considérablement le couplage.
En résumé, le pattern de stratégie :
- Le pattern de stratégie est une méthode qui définit une série d'algorithmes (tous effectuant le même travail : remise, rabais conditionnel, tarif normal) mais avec des implémentations différentes. Tous peuvent être appelé de la même manière, ce qui réduit le couplage entre les algorithmes.
- Ce pattern permet de définir chaque algorithme indépendamment et de les rendre interchangeables. Le client peut choisir l'algorithme approprié sans modifier son code.
- L'utilisation du pattern stratégie avec le pattern factory permet de masquer complètement les détails d'implémentation au client, offrant ainsi une meilleure encapsulation et maintenabilité.