Le pattern strategy version gastronomique


Allez, un petit article de POO un peu avancée pour faire marcher ses neurones ce WE.

Le design pattern strategy, qui consiste à déléguer une partie du comportement d’un objet à un autre objet, est probablement l’un des motifs de conception les plus utiles en programmation. Trop souvent les gens utilisent l’héritage là où la composition serait plus adaptée, et une injection de dépendance bien faite permet de gagner beaucoup en qualité de code.

Si vous ne vous souvenez plus de ce qu’est le pattern strategy, vous pouvez faire un saut sur le chapitre qui en parle dans le guide de la POO :)

Mais comme un petit rappel ne fait pas de mal, en très court, strategy ressemble à ça :

class MonObjet:
    def __init__(self):
        self.strategie = MaStrategie()
 
    def foo(self):
        return self.strategie.foo()

Ce qui permet à une sous-classe de changer la strategy ou non :

class MonSousObjet(MonObjet):
   def __init__(self):
        self.strategie = MonAutreStrategie()

Ou de changer la strat dynamiquement :

hop = MonObjet()
 
hop.stategie = SuperNewStrat()

Mais si vous vous en tenez à ce design, les utilisateurs de la classe vont très vite rencontrer des limitations.

D’abord, une bonne stratégie peut avoir besoin de contexte. Dans ce cas, donnez lui le choix d’avoir une référence à l’objet parent:

class MonObjet:
    def __init__(self):
        # Passer self permet à la stratégie de connaître son contexte. 
        # Le désavantage est l'introduction potentiel d'un couplage entre 
        # les deux objets, et potentiellement des effets de bords supplémentaires. Cela reste 
        # néanmoins souvent une bonne idée.
        self.strategie = MaStrategie(self)   
    ...

Ensuite, une stratégie devrait pouvoir être passée à la création de l’objet :

class MonObjet:
    def __init__(self, strategie=MaStrategie):
        # On donne la priorité à l'objet passé en paramètre. Si il n'y en a 
        # pas on crée la stratégie par défaut.
        self.strategie = strategie(self)  
    ...

Cela permet d’overrider la stratégie pour les usages plus avancés, tout en permettant aux débutants de ne pas se soucier de cela car il existe quand même une valeur par défaut.

truc = MonObjet(UneStrategieDifferente)

Comme le travail dans un init est souvent assez redondant, avoir un endroit pour permettre aux sous-classes de facilement overrider la stratégie est une bonne pratique. En Python il est courant d’utiliser les variables de classes pour cela :

class MonObjet:
 
    # On appelle souvent cet attribut "strategy_class" ou "strategy_factory"
    strategie_par_default = MaStrategie
 
    def __init__(self, strategie=None):
        self.strategie = strategie(self) if strategie else self.strategie_par_default(self) 
    ...
 
class MonSousObjet(MonObjet):
    # Et boom, overriding de la stratégie par la classe enfant en une ligne.
    # Django fait ça par exemple avec les classes based views et l'attribut 
    # model
    strategie_par_default = MonAutreStrategie

Une fois que vous avez fait tout ça, vous avez déjà fait mieux que 90% des programmeurs. Néanmoins si vous voulez vraiment mettre la petit touche pro à votre API, vous pouvez aussi permettre la création dynamique de la stratégie:

class MonObjet:
 
    strategie_par_default = MaStrategie
 
    def __init__(self, strategie=None):
        self.strategie = strategie(self) if strategie else self.build_strategy() 
 
    def build_strategy(self):
        return self.strategie_par_default(self)
 
    ...

Wow, ça en fait des self et des factories :) En fait, ça fait la même chose qu’avant, c’est à dire que le tout premier exemple de code tout simple qu’on a vu en début d’article marche toujours ! C’est la beauté de la chose.

La différence, c’est que maintenant une classe enfant peut overrider build_strategy() et créer des stratégies à la volée, en fonction du contexte d’exécution. Par exemple créer une stratégie différente en fonction d’une valeur de base de données. C’est rare que ça arrive, et c’est vraiment de l’usage avancé. Mais quand vous avez ça, vous êtes certains que votre code est prêt à être utilisé par autrui. Car si cet autrui n’est pas content, il peut faire une profonde coloscopie à votre code et y insérer ce qu’il veut, quand il veut.

Être dev après tout, c’est être un peu poète.

9 thoughts on “Le pattern strategy version gastronomique

  • azzra

    Merci pour l’article! Quelques coquilles à mon avis:

    POO un peu avancé -> avancée

    et créer des stratégies à la volées -> volée

    C’est rare que ça arrive, est c’est vraiment -> et

  • Cym13

    Pourquoi utiliser un ternaire plutôt que la valeur par défaut du paramètre dans le constructeur de l’objet pour exprimer une valeur par défaut ? On perd en auto-documentation et en concision.

  • abcd

    Intéressant !

    Par contre, dans l’avant dernier exemple, ce ne serait pas par hasard

            self.strategie = strategie(self) if strategie else self.strategie_par_default(self)
    
  • LandReagan

    Pour que la stratégie par défaut soit un attribut de la classe mère, non ? Comme ça, au choix, tu la gardes telle que dans la classe fille ou tu l’overrides par un attribut de classe de la fille. Tu appelles super().init() sans rien changer. Et ça marche dans toute la descendance.

    Ou je dis une connerie ?

  • Sam Post author

    @Cym13 : comme le dit LandReagan, dans l’exemple 4 et 5, c’est pour permettre aux classes filles d’overrider l’attribut de classe et donc la valeur par défaut. Dans l’exemple 3, c’était juste pour être consistant avec le reste et faciliter la compréhension mais je vois que ça n’aide pas donc je vais changer.

  • Stéphane

    strategie_par_default étant une constante, il faudrait que ce soit plutôt en majuscule (STRATEGIE_PAR_DEFAULT), non?

  • Lenma

    Merci pour ce bonne article ;-)

    Petit question pour pousser ce design pattern un peu plus loin : je sais que les exemples doivent êtres courts pour aller à l’essentiel mais dans ce genre de stratégie ne serait il pas approprié de créer une classe abstraite pour MaStrategie avec toutes les méthodes requise par la classe MonObjet et tester que MaStrategie ou MonAutreStrategie (ou n’importe quoi d’autre) hérite bien de cette classe abstraite.

    Cela permettrai de fournir une API plus sûr pour quiconque voudrait écrire sa propre classe stratégie.

    J’ai écris un exemple pour illustrer mon propos mais je suis dans l’embarra car je ne trouve pas la balise approprier pour afficher le code python proprement dans les commentaires, du coup c’est pas terriblement fonctionnel vu que des indentations disparaissent…

    from abc import ABCMeta, abstractmethod
    
    
    # Classe abstraite qui ne peut être instanciée
    class MaStrategieAbstraite(object):
    
        # Metaclass associé au décorateur abstractmethod
        __metaclass__ = ABCMeta
    
        def __init__(self, parent):
            super(MaStrategieAbstraite, self).__init__()
            # ...
    
        # Décorateur qui oblige a réécrire la méthode dans les classes filles.
        # Il ne faut pas oublier de définir la méta-classe ABCMeta pour que ça
        # fonctionne.
        @abstractmethod
        def foo(self, arg1, arg2):
            """ Cette méthode prend deux entiers et revois la somme des deux. """
            pass
    
    
    class MaStrategie(MaStrategieAbstraite):
    
        # Si cette méthode n'est pas défini il sera impossible de créer des
        # instances de cette classe.
        def foo(self, arg1, arg2):
            return arg1 + arg2
    
    
    class MonObjet:
        # On appelle souvent cet attribut "strategy_class" ou "strategy_factory"
        strategie_par_default = MaStrategie
    
        def __init__(self, strategie=None):
            strategie = strategie if strategie else self.strategie_par_default
    
            # On test le lien de parenté pour être sûr qu'il n'y aura pas de
            # problème de méthodes manquantes par la suite
            if not issubclass(strategie, MaStrategieAbstraite):
                raise TypeError("C'est pour ton bien mon enfant !")
    
            self.strategie = strategie(self)
    
        def addition(self, a, b):
            return self.strategie.foo(a, b)
    
    
    # On pourrais aussi hériter de `MaStrategie` si il y a plusieurs
    # methodes abstraites et qu'on veut n'en redéfinir qu'une seule.
    class MonAutreStrategie(MaStrategieAbstraite):
    
        def foo(self, arg1, arg2):
            print('%i + %i = %i' % (arg1, arg2, arg1+arg2))
            return arg1 + arg2
    
    
    class MonSousObjet(MonObjet):
        # Et boom, overriding de la stratégie par la classe enfant en une ligne.
        # Django fait ça par exemple avec les classes based views et l'attribut
        # model
    
        strategie_par_default = MonAutreStrategie
    
    
    # Exemples en action
    # ------------------
    
    mon_objet = MonObjet()
    mon_objet.addition(1, 2)  # Revois 3
    
    mon_sous_objet = MonSousObjet()
    mon_sous_objet.addition(1, 2)  # affiche '1 + 2 = 3' et revois 3
    
    
    class MauvaiseExampleDeStrategie(MaStrategieAbstraite):
        pass
    
    
    class AutreMauvaiseExampleDeStrategie(object):
        pass
    
    # Lève une exception (TypeError) puique la méthode foo n'a pas été redéfinie
    mon_objet2 = MonObjet(strategie=MauvaiseExampleDeStrategie)
    
    # Lève une exception (TypeError) puique la strategie n'hérite pas de `MaStrategieAbstraite`
    mon_objet3 = MonObjet(strategie=MauvaiseExampleDeStrategie)
    
    
  • Sam Post author

    @Stéphane : oui

    @Lenma : c’est en effet assez courant d’utiliser des classes abstraites avec strategy

    @Brice: la strategy peut être un objet complet avec plusieurs methodes.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.