Motifs de conception
Définition
1. Positionnement par rapport à la notion d’objet
Un objet ne fait pas l’application. Ce qui compte autant que les mécanismes permettant de travailler sur les objets, ce sont ceux qui permettent de gérer la manière dont ils interagissent en réponse à une problématique et à destination d’une fonctionnalité.
Pour ce faire, on utilise, consciemment ou non, des motifs de conception qui, chacun, correspondent à une méthode pour faire interagir des objets entre eux. Il y en a de plusieurs sortes, destinées à gérer des problématiques qui peuvent sembler parfois similaires mais qui ont toujours en réalité un contexte ou une destination particulière. Ils sont tellement nombreux qu’il faudrait un livre uniquement pour les présenter tous et cela n’aurait qu’un intérêt limité.
La plupart d’entre eux sont conçus pour répondre à une problématique précise. Ils sont la synthèse de retours d’expérience importants et significatifs et sont décrits avec précision par l’utilisation de plusieurs objets dont on décrit les rôles et les interactions.
Leur connaissance permet de standardiser la conception logicielle, de mettre en place des outils pour reproduire les bonnes solutions et d’améliorer...
Motifs de création
1. Singleton
Un singleton est, en mathématiques, un ensemble ne contenant qu’un seul élément. Avant que les patrons de conception ne soient formalisés, ce vocabulaire existait en Python pour des ensembles, listes ou n-uplets d’un seul élément et le mot singleton est utilisé dans la documentation officielle avec ce sens lié aux mathématiques.
Dans la documentation officielle, le patron de conception Singleton est implémenté en utilisant un module et en positionnant les données partagées en son sein (https://docs.python.org/3/faq/programming.html#how-do-i-share-global-variables-across-modules) :
# config.py:
x = 0 # Default value of the 'x' configuration setting
# mod.py:
import config
config.x = 1
# main.py:
import config
import mod
print(config.x)
En informatique, de manière générale, il s’agit d’une classe ne possédant qu’une seule instance. En Python, il existe le singleton None qui est de la classe NoneType :
>>> NoneType = type(None)
>>> NoneType()
None
Dans le modèle objet, le patron de conception peut être implémenté ainsi :
>>> class Singleton:
... _instance = None
... def __new__(cls):
... if cls._instance is None:
... cls._instance = object.__new__(cls)
... return cls._instance
...
>>> object() is object(), Singleton() is Singleton()
(False, True)
Il faut noter que, la plupart du temps, les langages de programmation utilisent le singleton pour pallier le fait que leur modèle objet n’est pas suffisamment souple pour pouvoir gérer ce que Python fait déjà à l’aide de ses méthodes de classes.
Ainsi, l’utilisation d’un singleton en Python est extrêmement rare.
Pour les programmeurs débutant en Python et ne connaissant pas encore la méthode __new__ ou les subtilités du modèle objet Python, ou bien pour ceux qui ont l’habitude d’un autre langage, la solution proposée se rapproche plus de ces autres langages :
>>> class Singleton:
... _instance = None ...
Motifs de structuration
1. Adaptateur
Présentation de la problématique
Pour avoir des traitements génériques, lorsque l’on conçoit une architecture, la solution permettant de disposer d’une interface commune et de créer les objets qui vont fournir cette interface est l’idéal.
Seulement, on travaille rarement uniquement avec des objets que l’on a conçus, on travaille également avec des bibliothèques tierces, ou des objets conçus préalablement qui sont adaptés à une problématique autre que la nôtre.
Dans tous les cas, il n’est pas possible de reprendre ces objets pour les faire entrer dans un moule qui satisfait parfaitement nos besoins.
Dans ce cadre-là, une solution largement diffusée est de créer des adaptateurs qui vont adapter le comportement des objets que l’on a à une interface unique.
Solution
Voici un exemple de classes qui sont parfaitement adaptées à une certaine utilisation et que nous souhaitons reprendre, mais utiliser d’une manière générique :
>>> class Chien:
... def aboyer(self):
... print('Ouaff')
...
>>> class Chat:
... def miauler(self):
... print('Miaou')
...
>>> class Cheval:
... def hennir(self):
... print('Hiiii')
...
>>> class Cochon:
... def grogner(self):
... print('Gruik')
...
Notre souhait est de faire « parler » ces animaux d’une manière générique.
Voici une classe, qui correspond à l’interface souhaitée :
>>> import abc
>>> class Animal(metaclass=abc.ABCMeta):
... @abc.abstractmethod
... def faireDuBruit(self):
... return
...
On pourrait alors reprendre les quatre classes précédentes et les réécrire avec la même méthode, mais cela entraînerait une perte au niveau sémantique alors que c’est potentiellement utile pour d’autres utilisations....
Motifs de comportement
1. Chaîne de responsabilité
Présentation de la problématique
Le motif de conception nommé chaîne de responsabilité permet de créer une chaîne entre différents composants qui traitent une donnée. Ainsi, chaque composant reçoit une donnée, la traite s’il le peut, et la transmet au composant suivant dans la chaîne, le tout sans se préoccuper de savoir si le message va intéresser son successeur ou pas.
Solution
Voici un composant autonome qui gère le traitement ou non d’une donnée en fonction de conditions qui lui sont passées à l’initialisation :
>>> class Composant:
... def __init__(self, name, conditions):
... self.name = name
... self.conditions = conditions
... self.next = None
... def setNext(self, next):
... self.next = next
... def traitement(self, condition, message):
... if condition in self.conditions:
... print('Traitement du message %s par %s' %
(message, self.name))
... if self.next is not None:
... self.next.traitement(condition, message)
...
Voici comment créer trois composants :
>>> c0 = Composant('c0', [1, 2])
>>> c1 = Composant('c1', [1])
>>> c2 = Composant('c2', [2])
Comment créer la chaîne de dépendance :
>>> c0.setNext(c1)
>>> c1.setNext(c2)
Et le résultat lorsque l’on donne une condition et un message :
>>> c0.traitement(1, 'Test 1')
Traitement du message Test 1 par c0
Traitement du message Test 1 par c1
>>> c0.traitement(2, 'Test 2')
Traitement du message Test 2 par c0
Traitement du message Test 2 par c2
Conséquences
Cette méthodologie est un moyen simple de créer un découplage entre fonctionnalités séquentiellement exécutées.
2. Commande
Présentation de la problématique
Le modèle objet présente...
ZCA
1. Rappels
La ZCA est la Zope Component Architecture et est un ensemble de bibliothèques indépendantes qui permettent de créer une architecture entre composants.
2. Adaptateur
Déclaration
Voici, déclarés conformément aux usages de la ZCA, deux interfaces et deux objets :
>>> from zope.interface import Interface
>>> from zope.interface import Attribute
>>> from zope.interface import implements
>>> class Ichien(Interface):
... nom = Attribute("""Nom du chien""")
... def aboyer(filename)
... """Méthode permettant de le faire aboyer"""
...
>>> class Chien(object):
... implements(IChien)
... nom = u''
... def __init__(self, nom):
... self.nom = nom
... def aboyer(self):
... """Méthode permettant de le faire aboyer"""
... print('Ouaff')
...
>>> class Ichat(Interface):
... nom = Attribute("""Nom du chat""")
... def miauler(filename):
... """Méthode permettant de le faire miauler"""
...
>>> class Chat(object):
... implements(IChat)
... nom = u''
... def __init__(self, nom):
... self.nom = nom
... def miauler(self):
... """Méthode permettant de le faire miauler"""
... print('Miaou')
...
L’idée de cet exemple est d’adapter ces deux objets au sein d’une seule et même classe dont on va commencer par créer une interface. Cette adaptation est simplement réalisée par l’utilisation de la fonction adapts.
Voici...