PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Le pattern strategy version gastronomique 9

vendredi 14 juillet 2017 à 21:11

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.