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. En informatique, il s’agit d’une classe ne possédant qu’une seule instance.
>>> 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.
2. Fabrique
Présentation de la problématique
Lorsque l’on a une classe mère abstraite et plusieurs classes filles, la classe mère abstraite permet de capitaliser les comportements communs à toutes les filles. Pour les langages statiquement typés, une fonctionnalité utile est de potentiellement travailler avec des objets déclarés comme étant du type de la mère de manière à ce que les comportements puissent être homogénéisés. En clair, que l’on ne soit pas obligé de dupliquer du code autant de fois qu’il y ait de filles.
Pour cela, on utilise une fabrique qui va alors prendre en paramètre les données nécessaires à la construction de l’objet, déterminer à partir de critères déterministes la classe fille à instancier, créer cette instance, et la renvoyer. Cette instance portera alors le type de sa mère (voir notion de polymorphisme).
Solution
Python est un langage à typage dynamique. Il ne dispose pas de contraintes lui imposant de travailler avec un type de données particulier - à moins que le développeur l’ait lui-même spécifié dans son code - et il utilise naturellement la notion de « Duck Typing ».
En ce sens, les problématiques purement techniques réglées...
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.
Python...
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...
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...