Design Patterns : Strategy

Nous avons pu découvrir précédemment le design patterns Singleton, à mes yeux le plus simple de tous.

Attaquons nous à un autre design patterns très utilisé…le Design Pattern Strategy

Contexte

C’est un grand nom de l’agroalimentaire qui nous contacte, il produit des biscuits. Il vient d’inventer une ligne de production totalement innovante qui peut, à la volée, changer le type de biscuit qu’elle produit.

Voici le cahier des charges :

Notre ligne de production révolutionnaire produit plusieurs types de biscuit, chacun avec une recette différente. La fabrication d’un biscuit contient deux phases : la première est la fabrication du biscuit lui-même, la deuxième le nappage que le biscuit reçoit. Nous souhaitons un logiciel qui puisse gérer cette ligne de production.

Analyse

D’un point de vue objet nous avons les objets de type biscuit, les algorithmes de nappage et de préparation des biscuits ainsi que la ligne de production. On commence par créer la classe de base des biscuits…tout simplement appelée Biscuit ! Cette classe est abstraite car bien entendu interdiction de l’instancier.

 public abstract class Biscuit
    {
        // Le nom commercial du biscuit
        public string NomCommercial { get; set; }

        // L'algorithme de préparation du biscuit
        public abstract void PreparerBiscuit();
        // L'algorithme de nappage du biscuit
        public abstract void NapperBiscuit();
    }

Codons maintenant un simulateur de ligne de production, tout simplement une liste d’objet de type biscuit sur laquelle nous itérerons et nous appellerons les méthodes PreparerBiscuit() et NapperBiscuit().

static void Main(string[] args)
        {
            IList LigneProduction = new List();

            foreach (Biscuit curBiscuit in LigneProduction.ToList())
            {
                curBiscuit.PreparerBiscuit();
                curBiscuit.NapperBiscuit();
                Console.WriteLine("Le biscuit {0} est prêt", curBiscuit.NomCommercial);
            }

            Console.ReadLine();
        }

Et voilà le tour est joué nous avons simulé notre ligne de production. Mais pour l’instant nous n’avons aucun biscuit “concret” à fabriquer.

Biscuit ChocoComplet

Notre premier type de biscuit est le “ChocoComplet”, aux céréales complètes et nappé de chocolat.

Voici comment nous le codons dans un premier temps :

public class BiscuitChocoComplet : Biscuit
    {
        public BiscuitChocoComplet()
        {
            this.NomCommercial = "ChocoComplet";
        }

        public override void PreparerBiscuit()
        {
            Console.WriteLine("Algo préparation Blé Complet");
        }

        public override void NapperBiscuit()
        {
            Console.WriteLine("Algo nappage Chocolat");
        }
    }

Ok on sait donc désormais fabriquer un biscuit ChocoComplet, nous avons bien implémenter les deux algorithmes (bien plus compliqués dans la réalité) que nous héritons de notre classe de base.

Le client nous donne un autre type de biscuit : le ChocoTendre.

Biscuit ChocoTendre

Dans la foulée nous codons cette nouvelle classe avec encore une fois l’implémentation des deux algos.

public class BiscuitChocoTendre : Biscuit
    {
        public BiscuitChocoTendre()
        {
            this.NomCommercial = "ChocoTendre";
        }

        public override void PreparerBiscuit()
        {
            Console.WriteLine("Algo préparation Blé Tendre");
        }

        public override void NapperBiscuit()
        {
            Console.WriteLine("Algo nappage Chocolat");
        }
    }

Reste à coder tout les autres types de biscuits que le client produit et le tour est joué nous voilà riche !

Oui mais ….

Il existe disons 30 types de biscuits avec un nappage chocolat…donc par trente fois nous allons répéter l’algorithme de nappage chocolat…si un jour nous devons changer une ligne de code il faudra la changer dans chacune des méthodes NapperBiscuit().

Chaque biscuit à le même comportement (behavior), un algorithme de Préparation et un algorithme de Nappage. Ces deux familles d’algorithmes varient selon le type du biscuit mais chaque algorithme peut être commun à plusieurs types de biscuits. Pour factoriser tout ça nous allons créer deux interfaces : IPreparationBehavior et INappageBehavior. Les classes implémentant ces interfaces représenteront les différents algorithmes existants.

public interface INappageBehavior
    {
        void Napper();
    }

 

public interface IPreparationBehavior
    {
        void Preparer();
    }

Maintenant codons deux implémentations pour chacune de ces interfaces, une classe NappageChocolatBehavior, une classe NappageVanilleBehavior, une classe PreparationCompletBehavior et enfin une classe PreparationTendreBehavior. On obtient au final les quatre classes suivantes :

    public class NappageChocolatBehavior : INappageBehavior
    {
        public void Napper()
        {
            Console.WriteLine("Algo nappage Chocolat");
        }
    }

    public class NappageVanilleBehavior : INappageBehavior
    {
        public void Napper()
        {
            Console.WriteLine("Algo nappage Vanille");
        }
    }

    public class PreparationCompletBehavior : IPreparationBehavior
    {
        public void Preparer()
        {
            Console.WriteLine("Algo préparation Blé Complet");
        }
    }

    public class PreparationTendreBehavior : IPreparationBehavior
    {
        public void Preparer()
        {
            Console.WriteLine("Algo préparation Blé Tendre");
        }
    }

L’appel aux méthode Preparer() et Napper() étant toujours le même autant factoriser tout ça dans notre classe de base Biscuit.

Profitons des interfaces pour modifier la classe Biscuit et la rendre totalement indépendante de toute implémentation :

public abstract class Biscuit
    {
        // Le nom commercial du biscuit
        public string NomCommercial { get; set; }

        // Propriétés stockant les algorithmes à utiliser
        public IPreparationBehavior AlgoPreparation { get; set; }
        public INappageBehavior AlgoNappage { get; set; }

        // On se contente donc d'appeler la méthode de notre propriété AlgoPrepration
        // Nous sommes totalement découplé de l'implémentation de l'algorithme
        public void PreparerBiscuit()
        {
            this.AlgoPreparation.Preparer();
        }

        public void NapperBiscuit()
        {
            this.AlgoNappage.Napper();
        }
    }

Désormais on peut donc supprimer les implémentations de PreparerBiscuit() et NapperBiscuit() dans nos classes concrètes. En revanche on ajoute le type d’algorithme à utiliser dans le constructeur de nos classes concrètes.

  public class BiscuitChocoComplet : Biscuit
    {
        public BiscuitChocoComplet()
        {
            this.NomCommercial = "ChocoComplet";
            this.AlgoPreparation = new PreparationCompletBehavior();
            this.AlgoNappage = new NappageChocolatBehavior();
        }
    }

Ceci améliore grandement l’architecture du logiciel, d’une part si un algorithme doit être modifié alors cette modification sera rapide à effectuer et se propagera automatiquement à tout les biscuits l’utilisant.

Ce qu’il est essentiel de comprendre c’est que notre objet BiscuitChocoComplet ne doit pas connaître l’implémentation de l’algorithme de nappage chocolat par exemple, le pattern Strategy permet justement cette indépendance en encapsulant une famille d’algorithme et en les rendant interchangeables.

Avec notre façon de faire nous pourrions également changer à l’exécution l’algorithme à utiliser pour tel ou tel type de biscuit.

Il ne reste plus qu’à tester tout ça !

static void Main(string[] args)
        {
            IList LigneProduction = new List();

            LigneProduction.Add(new BiscuitChocoComplet());
            LigneProduction.Add(new BiscuitChocoTendre());
            LigneProduction.Add(new BiscuitChocoComplet());

            foreach (Biscuit curBiscuit in LigneProduction.ToList())
            {
                curBiscuit.PreparerBiscuit();
                curBiscuit.NapperBiscuit();
                Console.WriteLine("Le biscuit {0} est prêt", curBiscuit.NomCommercial);
            }

            Console.ReadLine();
        }

Définition du Pattern Strategy :

Defines a family of algorithms, encaspulates each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Diagramme de classe :

Diagramme de classe du pattern Strategy

Leave a Reply