SOLDES de printemps

Stratégie

Alias : Politique, Strategy

Intention

Stratégie est un patron de conception comportemental qui permet de définir une famille d’algorithmes, de les mettre dans des classes séparées et de rendre leurs objets interchangeables.

Patron de conception stratégie

Problème

Un beau jour, vous avez décidé de créer une application de navigation pour les voyageurs occasionnels. Vous l’avez développé avec une superbe carte comme fonctionnalité principale, qui aide les utilisateurs à s’orienter rapidement dans n’importe quelle ville.

La fonctionnalité la plus demandée était la planification d’itinéraire. Un utilisateur devrait pouvoir entrer une adresse et le chemin le plus rapide pour arriver à destination s’afficherait sur la carte.

La première version de l’application ne pouvait tracer des itinéraires que sur les routes. Les automobilistes étaient comblés. Mais apparemment, certaines personnes préfèrent utiliser d’autres moyens de locomotion pendant leurs vacances. Vous avez ajouté la possibilité de créer des trajets à pied dans la version suivante. Juste après cela, vous avez ajouté la possibilité d’utiliser les transports en commun dans les itinéraires.

Mais tout ceci n’était que le début. Vous avez continué en adaptant l’application pour les cyclistes, et plus tard, ajouté la possibilité de construire les itinéraires en passant par les attractions touristiques de la ville.

Le code du navigateur grossit à vue d’œil

Le code du navigateur grossit à vue d’œil.

L’application a beau avoir très bien marché d’un point de vue financier, vous vous êtes arraché les cheveux sur le côté technique. Chaque fois que vous ajoutiez un nouvel algorithme pour tracer les itinéraires, la classe principale Navigateur doublait de taille. À un moment donné, la bête n’était plus possible à maintenir.

Que ce soit pour corriger un petit problème ou pour ajuster les scores des rues, la moindre touche apportée aux algorithmes impactait la totalité de la classe, augmentant les chances de créer des bugs dans du code qui fonctionnait très bien.

De plus, travailler en équipe n’était plus efficace. Les membres de votre équipe embauchés juste après la sortie et le succès de votre application se plaignaient de passer trop de temps à résoudre des problèmes de fusion. Ajouter une nouvelle fonctionnalité vous demandait de modifier une classe énorme, créant des conflits dans le code produit par les autres développeurs.

Solution

Le patron de conception stratégie vous propose de prendre une classe dotée d’un comportement spécifique mais qui l’exécute de différentes façons, et de décomposer ses algorithmes en classes séparées appelées stratégies.

La classe originale (le contexte) doit avoir un attribut qui garde une référence vers une des stratégies. Plutôt que de s’occuper de la tâche, le contexte la délègue à l’objet stratégie associé.

Le contexte n’a pas la responsabilité de la sélection de l’algorithme adapté, c’est le client qui lui envoie la stratégie. En fait, le contexte n’y connait pas grand-chose en stratégies, c’est l’interface générique qui lui permet de les utiliser. Elle n’expose qu’une seule méthode pour déclencher l’algorithme encapsulé à l’intérieur de la stratégie sélectionnée.

Le contexte devient indépendant des stratégies concrètes. Vous pouvez ainsi modifier des algorithmes ou en ajouter de nouveaux sans toucher au code du contexte ou aux autres stratégies.

Stratégies de planification d’itinéraire

Stratégies de planification d’itinéraire.

Dans notre application de navigation, chaque algorithme d’itinéraire peut être extrait de sa propre classe avec une seule méthode tracerItinéraire. La méthode accepte une origine et une destination, puis retourne une liste de points de passage.

Quand bien même les différentes classes itinéraire ne donneraient pas un résultat identique avec les mêmes paramètres, la classe navigateur principale ne se préoccupe pas de l’algorithme sélectionné, car sa fonction première est d’afficher les points de passage sur la carte. La classe navigateur possède une méthode pour changer la stratégie d’itinéraire active afin que ses clients (les boutons de l’interface utilisateur par exemple) puissent remplacer le comportement sélectionné par un autre.

Analogie

Différentes stratégies de transport

Différentes stratégies pour se rendre à l’aéroport.

Imaginez que vous devez vous rendre à l’aéroport. Vous pouvez prendre le bus, appeler un taxi ou enfourcher votre vélo. Ce sont vos stratégies de transport. Vous pouvez sélectionner une de ces stratégies en fonction de certains facteurs, comme le budget ou les contraintes de temps.

Structure

Structure du patron de conception stratégieStructure du patron de conception stratégie
  1. Le Contexte garde une référence vers une des stratégies concrètes et communique avec cet objet uniquement au travers de l’interface stratégie.

  2. L’interface Stratégie est commune à toutes les stratégies concrètes. Elle déclare une méthode que le contexte utilise pour exécuter une stratégie.

  3. Les Stratégies Concrètes implémentent différentes variantes d’algorithmes utilisées par le contexte.

  4. Chaque fois qu’il veut lancer un algorithme, le contexte appelle la méthode d’exécution de l’objet stratégie associé. Le contexte ne sait pas comment la stratégie fonctionne ni comment l’algorithme est lancé.

  5. Le Client crée un objet spécifique Stratégie et le passe au contexte. Le contexte expose un setter qui permet aux clients de remplacer la stratégie associée au contexte lors de l’exécution.

Pseudo-code

Dans cet exemple, le contexte utilise plusieurs Stratégies pour lancer diverses opérations arithmétiques.

// L’interface stratégie déclare les traitements communs à
// toutes les versions supportées de l’algorithme. Le contexte
// utilise cette interface pour appeler l’algorithme défini par
// les stratégies concrètes.
interface Strategy is
    method execute(a, b)

// Les stratégies concrètes implémentent l’interface de base
// Stratégie et hébergent l’algorithme. L’interface les rend
// interchangeables dans le contexte.
class ConcreteStrategyAdd implements Strategy is
    method execute(a, b) is
        return a + b

class ConcreteStrategySubtract implements Strategy is
    method execute(a, b) is
        return a - b

class ConcreteStrategyMultiply implements Strategy is
    method execute(a, b) is
        return a * b

// Le contexte définit l’interface dont les clients ont besoin.
class Context is
    // Le contexte maintient une référence à l’un des objets
    // Stratégie. Le contexte ne connait pas la classe concrète
    // de la stratégie. Il doit manipuler toutes les stratégies
    // via l’interface stratégie.
    private strategy: Strategy

    // En général, le contexte accepte une stratégie en
    // paramètre du constructeur et fournit un setter pour
    // permettre de changer de stratégie lors de l’exécution.
    method setStrategy(Strategy strategy) is
        this.strategy = strategy

    // Le contexte délègue certaines tâches à l’objet stratégie,
    // plutôt que d’implémenter plusieurs versions de
    // l’algorithme.
    method executeStrategy(int a, int b) is
        return strategy.execute(a, b)


// Le code client choisit une stratégie concrète et la passe au
// contexte. Le client doit connaitre les différences entre les
// stratégies afin de faire le bon choix.
class ExampleApplication is
    method main() is
        Create context object.

        Read first number.
        Read last number.
        Read the desired action from user input.

        if (action == addition) then
            context.setStrategy(new ConcreteStrategyAdd())

        if (action == subtraction) then
            context.setStrategy(new ConcreteStrategySubtract())

        if (action == multiplication) then
            context.setStrategy(new ConcreteStrategyMultiply())

        result = context.executeStrategy(First number, Second number)

        Print result.

Possibilités d’application

Utilisez le patron de conception stratégie si vous voulez avoir différentes variantes d’un algorithme à l’intérieur d’un objet à disposition, et pouvoir passer d’un algorithme à l’autre lors de l’exécution.

Ce patron vous permet de modifier indirectement le comportement de l’objet lors de l’exécution, en l’associant avec différents sous-objets qui peuvent accomplir des sous-tâches spécifiques de différentes manières.

Utilisez la stratégie si vous avez beaucoup de classes dont la seule différence est leur façon d’exécuter un comportement.

Ce patron vous permet d’extraire des variantes d’un comportement dans une hiérarchie de classes séparées et de combiner les classes originales dans une seule, évitant de dupliquer du code.

Utilisez la stratégie pour isoler la logique métier d’une classe, de l’implémentation des algorithmes dont les détails ne sont pas forcément importants pour le contexte.

Ce patron vous permet de séparer le code, les données internes et les dépendances des divers algorithmes du reste du code. Une interface toute simple permet aux clients d’exécuter les algorithmes et d’en changer lors de l’exécution.

Utilisez ce patron si votre classe possède un gros bloc conditionnel qui choisit entre différentes variantes du même algorithme.

La stratégie vous débarrasse de toutes ces conditions en extrayant tous les algorithmes dans des classes séparées, et ces dernières implémentent toutes la même interface. L’objet original délègue l’exécution à l’un de ces objets, au lieu d’implémenter toutes les variantes de l’algorithme.

Mise en œuvre

  1. Dans la classe contexte, identifiez un algorithme qui varie souvent. Il peut s’agir d’un gros bloc conditionnel qui sélectionne une variante du même algorithme lors de l’exécution.

  2. Déclarez l’interface stratégie commune à toutes les variantes de l’algorithme.

  3. Extrayez tous les algorithmes un par un et mettez-les dans leurs propres classes. Elles doivent toutes implémenter l’interface stratégie.

  4. Ajoutez un attribut pour garder une référence vers un objet stratégie dans la classe contexte. Créez un setter pour modifier le contenu de cet attribut. Le contexte ne doit manipuler l’objet stratégie qu’au travers de l’interface stratégie. Le contexte peut définir une interface qui laisse la stratégie accéder à ses données.

  5. Les clients d’un contexte doivent l’associer avec une stratégie adaptée au comportement attendu.

Avantages et inconvénients

  • Vous pouvez permuter l’algorithme utilisé à l’intérieur d’un objet à l’exécution.
  • Vous pouvez séparer les détails de l’implémentation d’un algorithme et le code qui l’utilise.
  • Vous pouvez remplacer l’héritage par la composition.
  • Principe ouvert/fermé. Vous pouvez ajouter de nouvelles stratégies sans avoir à modifier le contexte.
  • Si vous n’avez que quelques algorithmes qui ne varient pas beaucoup, nul besoin de rendre votre programme plus compliqué avec les nouvelles classes et interfaces qui accompagnent la mise en place du patron.
  • Les clients doivent pouvoir comparer les différentes stratégies et choisir la bonne.
  • De nombreux langages de programmation modernes gèrent les types fonctionnels et vous permettent d’implémenter différentes versions d’un algorithme à l’intérieur d’un ensemble de fonctions anonymes. Vous pouvez ensuite utiliser ces fonctions exactement comme vous le feriez pour des objets stratégie, sans encombrer votre code avec des classes et interfaces supplémentaires.

Liens avec les autres patrons

  • Le Pont, l’État, la Stratégie (et dans une certaine mesure l’Adaptateur) ont des structures très similaires. En effet, ces patrons sont basés sur la composition, qui délègue les tâches aux autres objets. Cependant, ils résolvent différents problèmes. Un patron n’est pas juste une recette qui vous aide à structurer votre code d’une certaine manière. C’est aussi une façon de communiquer aux autres développeurs le problème qu’il résout.

  • La Commande et la Stratégie peuvent se ressembler, car vous les utilisez toutes les deux pour paramétrer un objet avec une action. Cependant, ces patrons ont des intentions très différentes.

    • Vous pouvez utiliser la commande pour convertir un traitement en un objet. Les paramètres du traitement deviennent des attributs de cet objet. La conversion vous permet de différer le lancement du traitement, le mettre dans une file d’attente, stocker l’historique des commandes, envoyer les commandes à des services distants, etc.

    • La stratégie quant à elle, décrit généralement différentes manières de faire la même chose et vous laisse permuter entre ces algorithmes à l’intérieur d’une unique classe contexte.

  • Le Décorateur vous permet de changer la peau d’un objet, alors que la Stratégie vous permet de changer ses tripes.

  • Le Patron de méthode est basé sur l’héritage : il vous laisse modifier certaines parties d’un algorithme en les étendant dans les sous-classes. La Stratégie est basée sur la composition : vous pouvez modifier certaines parties du comportement de l’objet en lui fournissant différentes stratégies qui correspondent à ce comportement. Le patron de méthode agit au niveau de la classe, il est donc statique. La stratégie agit au niveau de l’objet et vous laisse permuter les comportements à l’exécution.

  • L’État peut être considéré comme une extension de la Stratégie. Ces deux patrons de conception sont basés sur la composition : ils changent le comportement du contexte en déléguant certaines tâches aux objets assistant. La stratégie rend ces objets complètement indépendants sans aucune visibilité l’un sur l’autre. Cependant, l’état n’impose pas de restrictions sur les dépendances entre les états concrets, et leur laisse modifier l’état du contexte à volonté.

Exemples de code

Stratégie en C# Stratégie en C++ Stratégie en Go Stratégie en Java Stratégie en PHP Stratégie en Python Stratégie en Ruby Stratégie en Rust Stratégie en Swift Stratégie en TypeScript