Le design pattern Strategy: une stratégie efficace pour gérer les différentes variantes d'algorithmes dans vos projets.

Introduction

Le design pattern Strategy, aussi connu sous le nom de Policy pattern, a été identifié pour la première fois par le Gang of Four (GoF) dans leur ouvrage intitulé "Design Patterns: Elements of Reusable Object-Oriented Software" (1994). Il fait partie des 23 design patterns de base décrits dans cet ouvrage.

Intention

Le design pattern Strategy permet de définir une famille d'algorithmes, de les encapsuler individuellement et de rendre ces algorithmes interchangeables. Cela signifie que le client peut choisir l'algorithme qu'il souhaite utiliser à l'exécution, sans avoir à connaître les détails de chaque algorithme.

Domaine d'application

On peut utiliser le design pattern Strategy lorsque:

  • de nombreuses classes apparentées ne diffèrent que par leur comportement. Les stratégies fournissent un moyen de configurer une classe avec l'un des nombreux comportements.
  • vous avez besoin de différentes variantes d'un algorithme.
  • un algorithme utilise des données que les clients ne doivent pas connaître: le pattern Strategy évite d'exposer des structures de données complexes et spécifiques à l'algorithme.
  • une classe définit de nombreux comportements, qui apparaissent sous la forme de multiples instructions conditionnelles dans ses opérations. Au lieu de plusieurs conditions, déplacez les branches conditionnelles connexes dans leur propre classe Strategy, en utilisant par exemple la technique de refactoring extract method suivi de la technique extract class.

Participants

  • Strategy: déclare une interface commune à tous les algorithmes pris en charge ; le contexte utilise cette interface pour appeler l'algorithme défini par une ConcreteStrategy
  • ConcreteStrategy: met en œuvre l'algorithme en utilisant l'interface Strategy
  • Context:
    • est configuré avec un objet ConcreteStrategy
    • maintient une référence à un objet Strategy
    • peut définir une interface qui permet à Strategy d'accéder à ses données.
_strategy
Context
-Strategy:_strategy
«interface»
Strategy
+Execute()
ConcreteStrategyA
+Execute()
ConcreteStrategyB
+Execute()
ConcreteStrategyC
+Execute()

Mise en œuvre

Voici un exemple basique de mis en œuvre du pattern:

  1. Définir une interface commune pour les algorithmes que le client souhaite utiliser. Cette interface définit les méthodes communes à tous les algorithmes.
public interface IStrategy {
    void Execute();
}

2. Créer une classe pour chaque algorithme. Ces classes implémentent l'interface et fournissent une implémentation spécifique pour chaque algorithme.

public class ConcreteStrategyA : IStrategy {
    public void Execute() {
        // Implémentation spécifique pour la stratégie A
    }
}

public class ConcreteStrategyB : IStrategy {
    public void Execute() {
        // Implémentation spécifique pour la stratégie B
    }
}

3. Créer une classe contexte qui permet au client de choisir l'algorithme à utiliser. Le contexte stocke une référence à l'algorithme choisi et utilise l'interface pour appeler l'algorithme.

public sealed class Context {

    private IStrategy _strategy;

    public Context(IStrategy strategy) {
        _strategy = strategy;
    }

    public void SetStrategy(IStrategy strategy) {
        _strategy = strategy;
    }

    public void ExecuteStrategy() {
        _strategy.Execute();
    }
}

Le client peut alors choisir l'algorithme à utiliser en instanciant la classe correspondante et en passant l'objet à la classe contexte. Le contexte peut également être configuré pour changer d'algorithme en cours d'exécution, en utilisant simplement une méthode de modification de l'algorithme stocké.

Context context = new Context(new ConcreteStrategyA());
context.ExecuteStrategy();
context.SetStrategy(new ConcreteStrategyB());
context.ExecuteStrategy();

Autres implémentations

La stratégie et le contexte peuvent interagir pour mettre en œuvre l'algorithme choisi. Ainsi, un contexte peut transmettre toutes les données requises par l'algorithme à la stratégie lorsque l'algorithme est appelé. Une alternative possible, le contexte peut se transmettre comme argument aux opérations de la stratégie, ce qui permet à la stratégie de faire appel au contexte si nécessaire.

Conséquences

Ce pattern peut être une alternative à l'héritage pour la définition de comportements variés, car il permet de changer de comportement en runtime en utilisant une stratégie différente, plutôt que de changer le comportement en modifiant la classe héritée. Cela peut être particulièrement utile dans les situations où il y a de nombreux comportements possibles qui ne sont pas tous liés de manière hiérarchique.

Avec le pattern Strategy, les traitements conditionnels peuvent être gérés en sélectionnant la stratégie appropriée en fonction des conditions. Par exemple, si vous avez plusieurs algorithmes différents qui peuvent être utilisés pour trier une liste d'éléments, vous pouvez utiliser des conditions pour déterminer quel algorithme de tri utiliser en fonction du nombre d'éléments à trier ou de la nature des éléments eux-mêmes.

Voici un exemple de code qui utilise le pattern Strategy pour sélectionner un algorithme de tri en fonction du nombre d'éléments à trier :

public interface ISorter {
    int[] Sort(int[] items);
}

public sealed class QuickSort : ISorter {
    public int[] Sort(int[] items) {
        // Implémentation de l'algorithme de tri rapide
    }
}

public sealed class MergeSort : ISorter {
    public int[] Sort(int[] items) {
        // Implémentation de l'algorithme de tri fusion
    }
}

public sealed sealed class MyArray {

    private ISorter _strategy = new QuickSort();
    private int[]   _values;

    public MyArray(params int[] values) {
        _values = values;
    }

    public int Length => _values.Length;

    public void DefineSortStrategy(ISorter strategy) {
        _strategy = strategy;
    }

    public int[] Sort() {
        return _strategy.Sort(_values);
    }
}

public static void Main() {
    var myArray = new MyArray(5, 2, 8, 1, 9);
    if (myArray.Length > 10) {
        myArray.DefineSortStrategy(new MergeSort());
    }
    myArray.Sort(items);
}

Dans cet exemple, la classe MyArray utilise l'interface ISorter pour définir une famille d'algorithmes de tri interchangeables. MyArray crée une instance de QuickSort et l'utilise comme stratégie de tri par défaut. Si la liste à trier comprend plus de 10 éléments, l'application modifie la stratégie a utiliser en utilisant l'algorithme de tri fusion à la place.

Désavantages

Voici quelques désavantages potentiels du pattern Strategy:

  • Les stratégies peuvent être redondantes : Dans certaines situations, il peut être difficile de déterminer si une stratégie existante peut être réutilisée ou s'il est nécessaire de créer une nouvelle stratégie. Cela peut entraîner un certain nombre de stratégies redondantes dans le code.
  • La gestion de la logique de sélection de la stratégie peut être complexe : Si vous avez de nombreuses stratégies possibles et que vous devez choisir la stratégie appropriée en fonction de nombreux critères différents, la gestion de cette logique de sélection peut devenir complexe et difficile à maintenir.
  • Surcharge de communication entre Strategy et Context: L'interface Strategy est partagée par toutes les classes ConcreteStrategy, que les algorithmes qu'elles mettent en œuvre soient triviaux ou complexes. Par conséquent, il est probable que certaines stratégies concrètes n'utilisent pas toutes les informations qui leur sont transmises par cette interface ; les stratégies concrètes simples n'en utilisent peut-être aucune ! Cela signifie qu'il y aura des moments où le contexte crée et initialise des paramètres qui ne seront jamais utilisés. Si c'est un problème, vous aurez besoin d'un couplage plus étroit entre la stratégie et le contexte.

Malgré ces désavantages, le pattern Strategy peut être une solution utile dans de nombreuses situations où vous avez besoin de changer de comportement en runtime sans utiliser l'héritage. C'est une question de trouver le bon équilibre entre les avantages et les inconvénients de ce pattern dans votre contexte spécifique.

Le design pattern Strategy et les tests

Le design pattern Strategy peut être utilisé pour faciliter le mockage des comportements lors des tests unitaires. Le test peut être configuré pour utiliser une implémentation concrète ou un mock de l'interface de comportement.

En utilisant cette approche, vous pouvez facilement tester différents comportements en injectant différentes implémentations de l'interface de comportement dans la classe à tester. Vous pouvez également utiliser un mock de l'interface de comportement pour vérifier que la classe à tester appelle les méthodes attendues de l'interface de comportement.

Références