Sommaire
Programmation Orientée Objet, Design/Architectural Patterns, … Vous en avez déjà entendu parler. Mais savez vous précisément ce en quoi ça consiste ?
Ces 8 articles vous permettront de comprendre les grands principes qui se cachent derrière ces termes techniques, mais également d’apprendre comment et pourquoi les utiliser.
- La Programmation Orientée Objet
- Introduction à UML : Diagrammes de classe
- Concepts de base de POO
- Les Patrons de Conception (Design Patterns)
- Les Patrons d’Architecture (Architectural Patterns)
- Concepts avancés de POO
- Qualité de Code
- Web Services
Introduction
Nous verrons dans ce chapitre ce qu’est un patron de conception, et comment les utiliser. Les patrons de conception étant très nombreux, nous n’aborderons que certains patrons de conception, parmi les 23 principaux listés par le Gang of Four dans un livre intitulé « Design Patterns: Elements of Reusable Object-Oriented Software ».
Qu’est-ce qu’un Patron de Conception ?
Un Patron de Conception constitue la meilleure solution connue à un problème de conception récurrent.
Analysons cette phrase afin de la rendre plus compréhensible.
Tout d’abord, on parle de “la meilleure solution”. Cela signifie que dans un but d’excellence, nous allons chercher, non pas une façon de faire, mais la meilleure façon de faire. De même, personne ne peut garantir qu’une solution est, et sera, toujours la meilleure. C’est pour cela qu’on précise “meilleure solution connue”. Les technologies ne cessent d’évoluer, et avec elle, les méthodes de conceptions. C’est pour cela qu’on parle de la meilleure solution connue aujourd’hui.
Oui mais pour faire quoi ?
Pour résoudre un problème de conception récurrent !
Récurrent, car peu de gens s’amusent à résoudre tous les problèmes du monde. Des développeurs, ou même des chercheurs se sont donc penchés sur des problèmes fréquemment rencontrés. Si vous vous demandez par exemple “comment faire pour qu’une classe ne puisse être instanciée qu’une seule et unique fois, et que cette instance puisse être partagée ?”, et bien il existe un patron de conception pour résoudre ce problème ! Certaines personnes se sont penchées sur ce problème qui est rencontré “fréquemment”, afin de réfléchir à une solution en termes de conception.
Les différents types de patron de conception
Ces Patrons de Conception peuvent être de trois types généralement :
- Création
- Structure
- Comportement
Un patron de conception est généralement représenté en UML avec un diagramme de classe.
Un patron de création a pour but de résoudre un problème lié à la création/configuration d’objets.
Un patron de structure se base sur la structure des classes/interface.
Un patron de comportement permet de résoudre des problèmes basés sur le comportement et les interactions entre classes.
Le patron Singleton
Ce patron est un des plus simples. Son but : garantir une instance unique. Il s’agit d’un patron de création, car il va gérer l’instanciation d’une classe afin de garantir son unicité.
Imaginons que vous vouliez créer une classe “DatabaseManager”, dont le but est de gérer la connexion à la base de données, et d’effectuer des requêtes. Votre application n’impliquant pas d’accès concurrent, vous vous dites “il faudrait que la connexion à la base de données ne se fasse qu’une seule fois, la première fois qu’on aura besoin de cette classe, et la déconnexion, lorsqu’on ne s’en sert plus”. De même, vous ne voulez pas avoir à créer une nouvelle instance de DatabaseManager chaque fois que vous aurez besoin d’effectuer des requêtes.
Le patron Singleton est une solution simple, permettant de gérer ce problème.
Tout d’abord, il suffit de définir le constructeur de la classe DatabaseManager en privé. De cette manière, personne d’autre que la classe elle-même ne pourra créer d’instance.
Ensuite, nous allons créer un attribut statique dans la classe DatabaseManager, de type DatabaseManager. Nous nommerons cet attribut “instance”. Cet attribut sera de même définit en “privé”, afin de limiter sa visibilité à la classe DatabaseManager uniquement. Cela veut dire que la classe contiendra un attribut statique de son propre type. Cela permet en gros de définir une variable globale, accessible à l’intérieure de DatabaseManager uniquement.
Enfin, nous allons créer une méthode publique, et statique, nommée “getInstance”. Cette méthode, comme son nom l’indique, permettra de retourner l’instance de DatabaseManager. Néanmoins, elle effectuera un petit traitement simple. Premièrement, cette méthode vérifiera si l’attribut “instance” est “null”. Si c’est le cas, elle instanciera “instance” avec un “new DatabaseManager()”. Cela fonctionnera, puisque c’est la classe qui effectue l’instanciation. Le constructeur peut donc être appelé. Enfin, cette méthode retournera “instance”.
En UML, cela donne ceci :
Et en code, cela donnerait quelque chose comme ceci :
Il ne reste plus qu’à ce que vous ajoutiez vos méthodes pour lui donner de l’intérêt (connexion à la base de données dans le constructeur, méthode pour effectuer une requête, etc…).
En terme d’utilisation, vous pourriez faire cela :
Lors du deuxième appel à “getInstance”, la même instance que pour le premier appel sera retournée.
Attention : il s’agit d’un des patrons les plus simples, néanmoins il est aujourd’hui catégorisé comme « anti-pattern ». Cela signifie qu’il crée d’autres problématiques. Néanmoins, d’autres patrons plus évolués offrent une meilleure solution, tels que l’injection de dépendance.
Le patron Fabrique
Vous vous souvenez des dépendances ? Il s’agit du terme qu’on utilise pour définir les liens entre différentes classes. Lorsque les dépendances sont fortes, le code est difficile à démêler et à faire évoluer, car comme son nom l’indique, la dépendance implique qu’une classe ne peut fonctionner sans d’autres classes.
Le patron Fabrique nous permet de simplifier cette gestion des dépendances, en créant tout simplement une classe, qui s’occupe d’instancier elle-même différents objets. Cela permet de définir toutes les dépendances dans cette fabrique, et de laisser à cette classe la responsabilité de créer divers objets. Il s’agit donc d’un patron de création.
Par exemple, si notre programme nécessite d’instancier des objets A et des objets B, on pourrait définir une méthode “createA()” et une méthode “createB()” dans la classe Fabrique. Ainsi, toutes les dépendances sont gérées par la Fabrique, et les autres classes du programme pourront faire appel à cette Fabrique pour instancier des objets.
Un diagramme UML parle souvent mieux que de longues phrases :
Sur ce diagramme, on voit clairement que la Fabrique (Factory) a pour responsabilité de gérer l’instanciation des objets de type “Class”. Il conviendra à l’algorithme défini dans “createClass()” de déterminer la classe à instancier (suivant certains paramètres par exemple).
Prenons un exemple un peu plus concret. Nous avons un jeu à coder, dans lequel il faut tuer des Zombies. Nous avons défini actuellement deux types de Zombie, les “ZombieStrong”, et les “ZombieWeak”. Notre “ZombieFactory” aura pour méthode “createZombie”, prenant un string en paramètre, afin de définir si l’on souhaite obtenir un zombi fort ou faible. Cette méthode retournera un nouvel objet “ZombieStrong” si “type” est égal à “strong”, ou un objet “ZombieWeak” si “type” est égal à “weak”. De cette manière, si un jour on souhaite remplacer tous les ZombieWeak par des “ZombieStrong” (si la difficulté a augmentée par exemple), la modification sera simple et peu coûteuse, car toutes les dépendances sont gérées dans notre Fabrique.
Le patron Décorateur
Dans certains cas, nous souhaitons pouvoir ajouter une fonction particulière à une classe qui n’était pas prévue pour à la base. Ce patron étant basé sur une structure, il s’agit d’un patron de structure.
Par exemple, si nous avons une classe “List” permettant tout simplement de créer et d’afficher une liste d’éléments, et que nous souhaitons pouvoir la “décorer” avec une “ScrollBar” (barre de défilement), le patron Décorateur résoudra parfaitement cette situation. De même, la “décoration” pourrait varier. On peut très bien imaginer pouvoir ajouter une “ScrollBar” dans un cas, ou un “SortFilter” dans un autre (pour pouvoir trier des éléments). Dans ce cas, nous allons ajouter une fonction à notre classe. Pourtant, suivant le concept de responsabilité, cela n’aurait aucun sens de définir toutes ces fonctionnalités dans la classe “List”. D’autant plus que nos “décorations” pourraient être pensées pour fonctionner aussi bien avec une “List” qu’avec un objet “Table”.
Ce Patron de Conception est particulier, car les rôles qu’il implique sont “inversés”. En effet, on aurait tendance à penser qu’on “pose” une “décoration” sur l’élément qui sera “décoré”. Or, avec le patron Décorateur, il s’agit de l’élément “décoré” qui est passé à la “décoration”.
Avec un exemple plus concret : Si nous avons effectivement créé notre “List”, et que nous souhaitons lui ajouter une fonctionnalité très puissante, à savoir la possibilité de retourner les éléments de la liste triés. Mais nous ne souhaitons pas modifier notre classe “List”.
La méthode “getElements” de la classe SortFilter ne retourne pas juste la liste des éléments de la liste, elle effectue auparavant un tri. De cette manière, la classe “List” a pu être “décorée” par une classe tierce.
Le patron Composite
Ce patron permet de gérer un objet, lui-même composé d’autres objets similaires à lui. Ce patron est lui aussi un patron de structure.
Par exemple, si vous devez gérer une arborescence de fichiers et de dossiers, vous devrez prendre en compte qu’un dossier peut-être composé de fichiers, mais aussi de dossiers. Il s’agit en quelque sorte d’une structure récursive. L’intérêt est de pouvoir gérer une infinité de niveaux.
Prenons comme exemple un site de e-commerce. Avec ce patron, vous pouvez gérer des familles d’articles, des sous-familles, des sous-sous-famille, etc… en ne définissant même qu’une seule classe. Cette classe “Famille” serait composée d’articles, mais aussi de “Famille”.
Le patron Observateur
Le patron Observateur résout le problème de l’attente active. Dans certains cas, il faut qu’un ou des traitements particuliers soient exécutés, mais à un moment précis, comme lorsqu’un objet effectue une action. La solution à ce problème se base sur le comportement des classes, il s’agit donc d’un patron de comportement.
Si vous, en tant qu’humain, souhaitez être au courant instantanément lorsqu’une autre personne effectue une action, comme lever la main par exemple, vous devez soit observer continuellement la personne en question, ou alors lui demander de vous prévenir lorsqu’il lève la main. Dans le premier cas, tant que vous observez cette personne, vous ne pouvez rien faire, sinon vous prendriez le risque de manquer le moment où la personne lèvera la main. Il s’agit de l’attente active.
Dans le second cas, vous pouvez retourner à vos occupations, car la personne vous préviendra lorsqu’elle lèvera la main. Il s’agit de la méthode employée par le patron Observateur.
La subtilité repose dans le fait que l’observateur n’observe pas le sujet. C’est le sujet qui contact l’observateur lorsque nécessaire.
Pour mettre en place ce patron, vous devez d’abord ajouter à votre sujet, une méthode permettant à des Observateurs de “s’inscrire”. Cette méthode ajoutera tous les objets dans une liste. Vous pouvez éventuellement définir une méthode de désinscription. Et enfin, la méthode qui déclenche les notifications. Cette dernière méthode devra notifier chaque Observateur, un par un.
Dans votre classe Observateur, vous n’aurez qu’à coder la méthode à exécuter pour qu’il soit notifié.
Prenons un exemple. Vous avez développé un Blog, et vous souhaitez pouvoir envoyer automatiquement des mails et des SMS aux utilisateurs inscrits, chaque fois qu’un nouvel article est publié.
Le patron Stratégie
Ce patron très simple permet d’interchanger divers objets, chacun pouvant se substituer. Cela permet de sélectionner l’algorithme à exécuter à la volée. Il s’agit d’un patron de comportement.
Par exemple, en tant de guerre, vous devez élaborer des stratégies, et suivant la situation, vous devez pouvoir exécuter la stratégie la plus adaptée. En programmation, toutes ces stratégies sont représentées par des classes. Celles-ci diffèrent uniquement par l’algorithme qu’elles implémentent.
Conclusion
Les patrons de conception sont des références qu’il peut être utile de connaître. Ils apportent une solution à un problème que l’on pourrait rencontrer. Le but n’est pas forcément de les appliquer bêtement, mais plutôt de comprendre la logique, et de l’adapter à une situation précise si cela est pertinent.
Pour résumer
Patron de Conception : La meilleure solution connue à un problème de conception récurrent.
Patron Singleton : Garantir une instance unique
Patron Fabrique : Donner à une classe la responsabilité d’instancier des objets.
Patron Décorateur : Ajouter de nouvelles fonctionnalités à une classe, sans la modifier.
Patron Composite : Gérer plusieurs objets comme s’il n’y en avait qu’un.
Patron Observateur : Permet à des classes d’être notifiées lorsqu’une classe effectue un traitement.
Patron Stratégie : L’algorithme exécuté est défini à la volée.