Notes inintéressantes sur les patrons de méthode.

Cet article est un concentré de notes écrites complètement à l'arrache pour parler plus profondément des patrons de méthode.

Mais comme je me dois de récompenser les 3 ou 4 lecteurs qui ont cliqué sur ce lien, voici une anecdote.

Le Monopoly, à la base appelé Le Jeu du propriétaire foncier, a été inventé par une femme, Elizabeth Magie, dans le but de dénoncer les dérives du capitalisme.

Le jeu est délibérément injuste dans le sens ou la personne ayant le plus de propriétés au début est destinée à accumuler des richesses, donc à acheter d'autres propriétés pour devenir encore plus riche, et ce au détriment des autres joueurs qui s'appauvrissent et sont obligés de lui vendre leurs propriétés pour rembourser leurs dettes, lui donnant ainsi le monopole.

Plutôt ironique, quand on sait que c'est aujourd'hui une des plus grosses machines à fric du monde des jeux de société.

Bref, trêve d'âneries, et parlons un peu plus technique.


Patron de méthode VS Stratégie

Le but des patrons de méthode est de modifier le comportement d'un module de l'extérieur en utilisant l'héritage. Mais si vous suivez mon blog, et en particulier mes bonus, vous savez déjà que ce n'est pas le seul moyen de faire ça.

Une autre approche très populaire est d'utiliser l'injection de dépendance avec une stratégie.

Le principe est d'avoir plusieurs objets qui implémentent tous les méthodes d'une même interface de manière différente (stratégie).

Ensuite, le module dont on veut changer le comportement va recevoir une instance de cette interface (injection de dépendance) et appeler ses méthodes (squelette). Et en fonction du type de l'objet, le comportement des méthodes ne sera pas le même.

Contrairement aux patrons de méthode, ce pattern favorise la composition plutôt que l'héritage.

Voici exactement le même programme que l'article, mais avec une stratégie plutôt qu'un patron de méthode :

public class MonopolyStrategy
{
    public MonopolyStrategy() { }

    public virtual void PlayerAction() =>
        Console.Write("Throw dices, move, buy and pay. ");

    public virtual void LoosingCondition() =>
        Console.Write("Go bankrupt. ");
}
Stratégie de base.
public class CheaterEditionMonopolyStrategy : MonopolyStrategy
{
    public CheaterEditionMonopolyStrategy() { }

    public override void PlayerAction() =>
        Console.Write("Throw dices, move, buy, pay and cheat a lot! ");
}

public class NoDeptMonopolyStrategy : MonopolyStrategy
{
    public NoDeptMonopolyStrategy() { }

    public override void LoosingCondition() =>
        Console.Write("Loosing all your cash. ");
}
Stratégie concrète.
public class Monopoly
{
    private MonopolyStrategy MonopolyStrategy;

    public Monopoly(MonopolyStrategy monopolyStrategy) =>
        MonopolyStrategy = monopolyStrategy;

    public void Play()
    {
        MonopolyStrategy.PlayerAction();
        MonopolyStrategy.LoosingCondition();
    }
}
Client des stratégies.
static void Main(string[] args)
{
    Monopoly monopoly = new Monopoly(new CheaterEditionMonopolyStrategy());
    monopoly.Play();  // Throw dices, move, buy, pay and cheat a lot! Go bankrupt. 
}
Client du monopoly.
Mais du coup, c'est quoi les avantages ?

Points forts des stratégies :

  • Elles sont souvent pertinentes pour les comportements génériques (algorithme de tri...), et peuvent donc être réutilisées autre part.
  • Elles peuvent changer facilement durant l'exécution, alors que pour un patron concret, vous êtes obligé de réinitialiser l'objet, et donc son état.

Points forts des patrons de méthode

  • Ils sont plus efficaces quand il s'agit de traiter le comportement d'un seul et même module.
  • Ils préservent mieux l'encapsulation que les stratégies, car vous n'injectez rien de l'extérieur. Tout est compris dans la logique du patron de base et de ses sous-types.

Choisir entre les deux n'est pas toujours facile, et ça dépendra énormément de votre contexte.

Par exemple, dans le cas du Monopoly, l'utilisation des patrons de méthode est pertinente, notamment parce que les étapes ne sont pas utilisables dans un autre contexte.

Mais aussi parce que conceptuellement, ça a plus de sens de considérer une variante du Monopoly comme étant une version alternative de celui-ci, plutôt qu'un… "jeu de règle introduit" ? Vous voyez, ça n'a pas grand sens.

En revanche, pour reprendre l'exemple de mon article sur l'injection de dépendances, les stratégies sont plus pertinentes, car ça a plus de sens de considérer un jeu vidéo comme un programme qu'on injecte dans une console, plutôt qu'une "variante de l'exécution de"… Bref, vous voyez ce que je veux dire.

Même si je ne suis pas vraiment fan de ce genre de réflexion, elles sont assez utiles pour ce choix.

Notez par ailleurs que les deux sont tout à fait utilisables en même temps. À vrai dire, il n'y a quasiment jamais d'incompatibilité entre les patrons de conception. Le vrai travail n'est pas de les implémenter, mais de trouver la bonne combinaison pour faire un programme aux petits oignons.

Plus d'informations par ici :

https://stackoverflow.com/questions/672083/when-to-use-template-method-vs-strategy

https://www.mysoftkey.com/design-pattern/design-pattern-strategy-vs-template-method/


Patron de méthode & Fabriques

Si vous avez lu mon article sur les fabriques, vous aurez (j'espère !) remarqué une similarité :

  1. Dans les fabriques, on a une classe de base qui contient une méthode abstraite Create(), et des classes concrètes qui l'implémentent.
  2. Dans la classe de base, on peut intégrer la méthode Create() dans d'autres pour obtenir des comportements plus complexes.
  3. Dans le client, on choisit le constructeur de sa fabrique concrète, et l’on appelle la méthode Create() (ou une autre qui l'utilise) pour obtenir une instance de l'objet désiré.

Ce n'est ni plus ni moins qu'un cas d'utilisation des patrons de méthode avec une seule étape (Create()). Allez lire mon article sur les fabriques pour plus de détails.


Faut-il bloquer les patrons concrets ?

Beaucoup de personnes recommandent d'empêcher l'héritage des patrons concret (via sealed en C# ou final en Java) afin de ne garder que deux niveaux d'abstraction.

En effet, si vous commencez à avoir des sous-types de vos patrons concrets, qui ont eux-mêmes des sous-types, etc. Ça va rendre votre programme beaucoup plus complexe que si toutes les classes héritent de la même base.

Mais est-ce vraiment obligatoire ?

Il y a certaines situations où vous allez travailler sur des algorithmes tellement complexes que bloquer des sous-types serait se limiter inutilement, et ceux-ci risquent d'avoir du code similaire.

Donc dans des cas simples, je pense que c'est un bon conseil, quitte à avoir quelques algorithmes qui se répètent dans les sous-types (que vous pouvez externaliser dans d'autres modules).

Mais si vous commencez à avoir des patrons de méthode de plus de 6 étapes, ça commence à être beaucoup trop complexe pour tout faire tenir dans une seule couche de patrons concrets.

Tout ça pour dire qu'il faut toujours adapter son nombre de couches d'abstraction en fonction de la taille du programme.

  • Si vous avez 4 couches pour un programme rikiki, ça va être une usine à gaz incompréhensible. On appelle ça la sur-ingénierie (overengineering).
  • Si vous n'avez que 2 couches sur un programme méga complexe, ça risque d'être un gros bordel bourré de duplication. On appelle ça la sous-ingénierie (underengineering).

Dans l'idéal, vous devez être bien au milieu.


Passer des paramètres à un patron concret

La condition de victoire de base du Monopoly, c'est que tout le monde perde. Mais dans une certaine variante, on aimerait que le vainqueur soit celui qui récupère le premier un certain nombre d'hôtels.

Pour rendre cette règle plus générique, on aimerait passer le nombre d'hôtels en paramètre. Mais c'est impossible avec les moyens traditionnels, car :

  • L'étape est protégée et donc inaccessible de l'extérieur.
  • L'étape ne prend déjà pas de paramètre dans le patron de base.

Alors, situation impossible ? Est-on obligé de commettre l'impardonnable en copiant-collant salement le jeu de règles de base ? Bien sûr que non.

L'astuce consiste à utiliser un champ privé dans le patron concret, que l'on vient alimenter à l'aide du constructeur. Ainsi, le patron concret dépend d'un paramètre extérieur, mais le patron de base n'y voit que du feu.

public class EmpireMonopolyTemplate : MonopolyTemplate
{
    private int HotelsToWin;

    public EmpireMonopolyTemplate(int hotelsToWin) =>
        HotelsToWin = hotelsToWin;

    protected override void WinningCondition()
    {
        if(CurrentPlayer.Hotels.Count >= hotelsToWin)
        {
            //...
        }
    }
}
static void Main(string[] args)
{
    MonopolyTemplate empireMonopoly = new EmpireMonopolyTemplate(10);
    empireMonopoly.Play();
}
Seul les paramètres du constructeur changent, l'appel de la méthode Play() est exactement le même.

Si vous vous souvenez bien, on avait utilisé la même solution lors de mon article sur les fabriques (j'aurais vraiment dû faire celui-ci en premier avec le recul ^^).


Instancier un patron concret

Dans mon exemple de code, j'avais instancié les patrons un par un pour vous montrer le résultat.

Mais généralement, votre client devra choisir entre le patron concret qu'il va utiliser, avec du code ce genre-là (les plus malins d'entre vous sauront déjà où je veux en venir) :

static void Main(string[] args)
{
    string arg = "";

    if(args.Length > 0)
        arg = args[0];

    MonopolyTemplate monopoly = arg?.ToUpper() switch {
        "CLASSIC" => new MonopolyTemplate(),
        "CHEATER" => new CheaterEditionMonopolyTemplate(),
        "NODEBT" => new NoDeptMonopolyTemplate(),
        _ => new MonopolyTemplate(),
    };

    monopoly.Play();
}

Une bonne pratique est d'utiliser une fabrique (oui, encore elles) pour instancier le patron que l'on souhaite, de sorte à déporter cet algorithme de sélection dans un module à part entière.

public abstract class MonopolyTemplateFactory
{
    public abstract MonopolyTemplate Create();
}

public class FromCommandArgsMonopolyTemplateFactory : MonopolyTemplateFactory
{
    private string ConsoleArgument;

    public FromCommandArgsMonopolyTemplateFactory(string[] args)
    {
        if(args.Length > 0)
            ConsoleArgument = args[0];
        else
            ConsoleArgument = "";
    }

    public override MonopolyTemplate Create()
    {
        return ConsoleArgument?.ToUpper() switch {
            "CLASSIC" => new MonopolyTemplate(),
            "CHEATER" => new CheaterEditionMonopolyTemplate(),
            "NODEBT" => new NoDeptMonopolyTemplate(),
            _ => new MonopolyTemplate(),
        };
    }
}
static void Main(string[] args)
{
    MonopolyTemplate monopoly = new FromCommandArgsMonopolyTemplateFactory(args).Create();
    monopoly.Play();
}

Encore une fois, un mélange astucieux de plusieurs patrons de conception.


Plusieurs squelettes dans une même classe

Imaginons que vous voulez changer le squelette tout en gardant les mêmes étapes. Autrement dit, faire l'inverse de ce que je vous ai présenté dans l'article.

Rien de plus facile, vous avez juste à ajouter un deuxième squelette à votre patron de base, et l'appeler dans le client. Ainsi, vous bénéficiez toujours des étapes des patrons concrets, mais avec une suite d'exécution différente.

public class MonopolyTemplate
{
    public MonopolyTemplate() { }

    // 1er squelette
    public void Play()
    {
        PlayerAction();
        LoosingCondition();
    }

    // 2nd squelette
    public void PlayMultipleTimes(int numberOfGames)
    {
        for(int i = 0; i < numberOfGames; i++)
        {
            PlayerAction();
            LoosingCondition();
        }
    }

    protected virtual void PlayerAction() =>
        Console.Write("Throw dices, move, buy and pay. ");

    protected virtual void LoosingCondition() =>
        Console.Write("Go bankrupt. ");
}

Cependant, il y a un hic. En faisant ça, vous avez infiniment plus de chances de briser le principe de substitution de Liskov (LSP).

Supposons que vous avez un patron de base avec une méthode squelette et 5 patrons concrets.

Si vous voulez ajouter une seconde méthode squelette, il faut vous assurer que les 5 sous-types fonctionnent tous avec elle. Autrement dit, doubler de volume votre jeu de tests.

De même, si vous ajoutez un nouveau patron concret, il faut que vous le testiez sur les deux méthodes squelettes.

Au pire, si un patron concret ne fonctionne pas avec la seconde méthode, ce n'est pas grave, le client ne l'utilisera jamais de toute façon.

Dans l'idée, ce n'est pas faux. Mais concrètement, vous vous jetez dans la gueule du loup, car vous admettez que si votre API n'est pas utilisée correctement, alors plus rien ne va fonctionner.

Le but de l'architecture logicielle est justement d'empêcher ça, car on ne sait jamais qui va reprendre votre code derrière. Et peu importe s’il s'agit d'un freelance, développeur senior, junior, stagiaire ou quelqu'un qui n'a jamais codé, notre but est de rendre son travail le plus simple et linéaire possible en interceptant tous les risques de mal utiliser l'API.

Il y a plein de manières de faire ça, comme :

  • L'encapsulation.
  • Les contrats d'API.
  • Le traitement des exceptions.
  • Une architecture qui ne permet pas les erreurs.

D'ailleurs, c'est pour moi une des plus grandes forces des langages fortement typés, mais c'est un sujet pour une prochaine fois 😉