CodeGym /Blog Java /Random-FR /Modèle de conception de stratégie
Auteur
John Selawsky
Senior Java Developer and Tutor at LearningTree

Modèle de conception de stratégie

Publié dans le groupe Random-FR
Salut! Dans la leçon d'aujourd'hui, nous parlerons du modèle de stratégie. Dans les leçons précédentes, nous nous sommes déjà brièvement familiarisés avec le concept d'héritage. Au cas où vous l'auriez oublié, je vous rappelle que ce terme fait référence à une solution standard à une tâche de programmation courante. Chez CodeGym, nous disons souvent que vous pouvez googler la réponse à presque toutes les questions. En effet, votre tâche, quelle qu'elle soit, a probablement déjà été résolue avec succès par quelqu'un d'autre. Les modèles sont des solutions éprouvées aux tâches les plus courantes ou des méthodes pour résoudre des situations problématiques. Ce sont comme des "roues" que vous n'avez pas besoin de réinventer par vous-même, mais vous devez savoir comment et quand les utiliser :) Un autre objectif des modèles est de promouvoir une architecture uniforme. Lire le code de quelqu'un d'autre n'est pas une tâche facile ! Tout le monde écrit un code différent, parce que la même tâche peut être résolue de plusieurs façons. Mais l'utilisation de modèles aide différents programmeurs à comprendre la logique de programmation sans se plonger dans chaque ligne de code (même en le voyant pour la première fois !) Aujourd'hui, nous examinons l'un des modèles de conception les plus courants appelé "Strategy". Modèle de conception : Stratégie - 2Imaginez que nous écrivions un programme qui fonctionnera activement avec des objets de transport. Peu importe ce que fait exactement notre programme. Nous avons créé une hiérarchie de classes avec une classe parent Conveyance et trois classes enfants : Sedan , Truck et F1Car .

public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {
}

public class Truck extends Conveyance {
}

public class F1Car extends Conveyance {
}
Les trois classes enfant héritent de deux méthodes standard du parent : go() et stop() . Notre programme est très simple : nos voitures ne peuvent qu'avancer et freiner. Poursuivant notre travail, nous avons décidé de donner aux voitures une nouvelle méthode : fill() (qui signifie "remplir le réservoir d'essence"). Nous l'avons ajouté à la classe parent Conveyance :

public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
  
   public void fill() {
       System.out.println("Refueling!");
   }
}
Des problèmes peuvent-ils vraiment survenir dans une situation aussi simple ? En fait, ils ont déjà... Modèle de conception : Stratégie - 3

public class Stroller extends Conveyance {

   public void fill() {
      
       // Hmm... This is a stroller for children. It doesn't need to be refueled :/
   }
}
Notre programme a maintenant un moyen de transport (une poussette) qui ne correspond pas bien au concept général. Il pourrait avoir des pédales ou être radiocommandé, mais une chose est sûre : il n'y aura pas de place pour verser de l'essence. Notre hiérarchie de classes a fait que des méthodes communes sont héritées par des classes qui n'en ont pas besoin. Que devons-nous faire dans cette situation ? Eh bien, nous pourrions remplacer la méthode fill() dans la classe Stroller afin que rien ne se passe lorsque vous essayez de faire le plein de la poussette :

public class Stroller extends Conveyance {

   @Override
   public void fill() {
       System.out.println("A stroller cannot be refueled!");
   }
}
Mais cela peut difficilement être qualifié de solution réussie si ce n'est pour une autre raison qu'un code en double. Par exemple, la plupart des classes utiliseront la méthode de la classe parente, mais les autres seront obligées de la remplacer. Si nous avons 15 classes et que nous devons remplacer le comportement de 5 à 6 d'entre elles, la duplication de code deviendra assez importante. Peut-être que les interfaces peuvent nous aider ? Par exemple, comme ceci :

public interface Fillable {
  
   public void fill();
}
Nous allons créer une interface Fillable avec une méthode fill() . Ensuite, les véhicules qui doivent être ravitaillés en carburant mettront en œuvre cette interface, tandis que d'autres véhicules (par exemple, notre poussette) ne le feront pas. Mais cette option ne nous convient pas. À l'avenir, notre hiérarchie de classes pourrait devenir très large (imaginez simplement combien de types de moyens de transport différents existent dans le monde). Nous avons abandonné la version précédente impliquant l'héritage, car nous ne voulons pas remplacer le fill()méthode de nombreuses fois. Maintenant, nous devons l'implémenter dans chaque classe ! Et si nous en avions 50 ? Et si des changements fréquents devaient être apportés à notre programme (et c'est presque toujours vrai pour les vrais programmes !), nous devions nous précipiter à travers les 50 classes et modifier manuellement le comportement de chacune d'entre elles. Alors que faire finalement dans cette situation ? Pour résoudre notre problème, nous allons choisir une autre voie. À savoir, nous allons séparer le comportement de notre classe de la classe elle-même. Qu'est-ce que cela signifie? Comme vous le savez, chaque objet a un état (un ensemble de données) et un comportement (un ensemble de méthodes). Le comportement de notre classe de transport se compose de trois méthodes : go() , stop() et fill() . Les deux premières méthodes sont très bien telles qu'elles sont. Mais nous allons sortir la troisième méthode duClasse de transport . Cela séparera le comportement de la classe (plus précisément, cela ne séparera qu'une partie du comportement, puisque les deux premières méthodes resteront là où elles sont). Alors, où devrions-nous mettre notre méthode fill() ? Rien ne me vient à l'esprit :/ On dirait que c'est exactement là où ça devrait être. Nous allons le déplacer vers une interface séparée : FillStrategy !

public interface FillStrategy {

   public void fill();
}
Pourquoi avons-nous besoin d'une telle interface ? Tout est simple. Nous pouvons maintenant créer plusieurs classes qui implémentent cette interface :

public class HybridFillStrategy implements FillStrategy {
  
   @Override
   public void fill() {
       System.out.println("Refuel with gas or electricity — your choice!");
   }
}

public class F1PitstopStrategy implements FillStrategy {
  
   @Override
   public void fill() {
       System.out.println("Refuel with gas only after all other pit stop procedures are complete!");
   }
}

public class StandardFillStrategy implements FillStrategy {
   @Override
   public void fill() {
       System.out.println("Just refuel with gas!");
   }
}
Nous avons créé trois stratégies comportementales : une pour les voitures ordinaires, une pour les hybrides et une pour les voitures de course de Formule 1. Chaque stratégie implémente un algorithme de ravitaillement différent. Dans notre cas, nous affichons simplement une chaîne sur la console, mais chaque méthode peut contenir une logique complexe. Que faisons-nous ensuite?

public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
  
}
Nous utilisons notre interface FillStrategy comme un champ dans la classe parent Conveyance . Notez que nous n'indiquons pas une implémentation spécifique - nous utilisons une interface. Les classes de voitures auront besoin d'implémentations spécifiques de l' interface FillStrategy :

public class F1Car extends Conveyance {

   public F1Car() {
       this.fillStrategy = new F1PitstopStrategy();
   }
}

public class HybridCar extends Conveyance {

   public HybridCar() {
       this.fillStrategy = new HybridFillStrategy();
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       this.fillStrategy = new StandardFillStrategy();
   }
}

Regardons ce que nous avons !

public class Main {

   public static void main(String[] args) {

       Conveyance sedan = new Sedan();
       Conveyance hybrid = new HybridCar();
       Conveyance f1car = new F1Car();

       sedan.fill();
       hybrid.fill();
       f1car.fill();
   }
}
Sortie console :

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Super! Le processus de ravitaillement fonctionne comme il se doit ! D'ailleurs, rien n'empêche d'utiliser la stratégie comme paramètre dans le constructeur ! Par exemple, comme ceci :

public class Conveyance {

   private FillStrategy fillStrategy;

   public Conveyance(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }

   public void fill() {
       this.fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       super(new StandardFillStrategy());
   }
}



public class HybridCar extends Conveyance {

   public HybridCar() {
       super(new HybridFillStrategy());
   }
}

public class F1Car extends Conveyance {

   public F1Car() {
       super(new F1PitstopStrategy());
   }
}
Exécutons notre méthode main() (qui reste inchangée). Nous obtenons le même résultat ! Sortie console :

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Le modèle de conception de stratégie définit une famille d'algorithmes, encapsule chacun d'eux et garantit qu'ils sont interchangeables. Il permet de modifier les algorithmes quelle que soit leur utilisation par le client (cette définition, tirée du livre "Head First Design Patterns", me semble excellente). Modèle de conception : Stratégie - 4Nous avons déjà spécifié la famille d'algorithmes qui nous intéresse (façons de faire le plein des voitures) dans des interfaces séparées avec différentes implémentations. Nous les avons séparés de la voiture elle-même. Maintenant, si nous devons apporter des modifications à un algorithme de ravitaillement particulier, cela n'affectera en rien nos classes de voitures. Et pour réaliser l'interchangeabilité, nous avons juste besoin d'ajouter une seule méthode setter à notre classe Conveyance :

public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }

   public void setFillStrategy(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }
}
Maintenant, nous pouvons changer de stratégie à la volée :

public class Main {

   public static void main(String[] args) {

       Stroller stroller= new Stroller();
       stroller.setFillStrategy(new StandardFillStrategy());

       stroller.fill();
   }
}
Si les poussettes commencent soudainement à fonctionner à l'essence, notre programme sera prêt à gérer ce scénario :) Et c'est à peu près tout ! Vous avez appris un modèle de conception de plus qui sera sans aucun doute essentiel et utile lorsque vous travaillez sur de vrais projets :) Jusqu'à la prochaine fois !
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION