Décorateur
Intention
Décorateur est un patron de conception structurel qui permet d’affecter dynamiquement de nouveaux comportements à des objets en les plaçant dans des emballeurs qui implémentent ces comportements.
Problème
Imaginez que vous travaillez sur une librairie qui permet aux programmes d’envoyer des notifications à leurs utilisateurs lorsque des événements importants se produisent.
La version initiale de la librairie était basée sur la classe Notificateur
qui n’avait que quelques attributs, un constructeur et une unique méthode envoyer
. La méthode pouvait prendre un message en paramètre et l’envoyait à une liste d’e-mails à l’aide du constructeur du notificateur. Une application externe qui jouait le rôle du client devait créer et configurer l’objet notificateur une première fois, puis l’utiliser lorsqu’un événement important se produisait.
Au bout d’un certain temps, vous vous rendez compte que les utilisateurs de la librairie veulent plus que les notifications qu’ils reçoivent sur leur boîte mail. Ils sont nombreux à vouloir recevoir des SMS lorsque leurs applications rencontrent des problèmes critiques. Certains voudraient être prévenus sur Facebook et les employés de certaines entreprises adoreraient recevoir des notifications Slack.
Cela n’a pas l’air bien difficile ! Vous avez étendu la classe Notificateur
et ajouté des méthodes de notification supplémentaires dans de nouvelles sous-classes.
Le client devait instancier la classe de notification désirée et utiliser cette instance pour toutes les autres notifications, mais quelqu’un a remis en question ce fonctionnement en vous affirmant qu’il aimerait bien utiliser plusieurs types de notifications simultanément. Si votre maison prend feu, vous avez très certainement envie d’en être informé par tous les moyens possibles.
Vous avez tenté de résoudre ce problème en créant des sous-classes spéciales qui combinent plusieurs méthodes de notification dans une seule classe. Mais il s’avère que cette approche va non seulement gonfler le code de la librairie, mais aussi celui du client.
Vous devez structurer vos classes de notification différemment pour ne pas trop les multiplier, sinon vous allez vous retrouver dans le livre Guinness des records.
Solution
La première chose qui nous vient à l’esprit lorsque l’on veut modifier le comportement d’un objet, c’est d’étendre sa classe. Cependant, voici quelques mises en garde concernant l’héritage :
- L’héritage est statique. Vous ne pouvez pas modifier le comportement d’un objet au moment de l’exécution. Vous ne pouvez que remplacer la totalité de l’objet par un autre, généré grâce à une sous-classe différente.
- Les sous-classes ne peuvent avoir qu’un seul parent. Dans la majorité des langages de programmation, vous ne pouvez hériter que d’une seule classe à la fois.
Une des solutions pour contourner ce problème consiste à utiliser l’aggrégation ou la composition à la place de l’héritage. Ces alternatives fonctionnent à peu près de la même façon : un objet possède une référence à un autre objet et lui délègue une partie de son travail. Avec l’héritage, l’objet est capable de faire le travail tout seul en héritant du comportement de sa classe mère.
Grâce à cette nouvelle approche, il est facile de remplacer l’objet référencé par un autre, ce qui modifie le comportement du conteneur au moment de l’exécution. Un objet peut utiliser le comportement de diverses classes, posséder des références à de nombreux objets et leur déléguer toutes sortes de tâches. L’agrégation et la composition sont des principes clés dans le décorateur, tout comme dans de nombreux autres patrons. Sur ces belles paroles, revenons à nos moutons.
Le décorateur est également appelé « emballeur » ou « empaqueteur ». Ces surnoms révèlent l’idée générale derrière le concept. Un emballeur est un objet qui peut être lié par un objet cible. L’emballeur possède le même ensemble de méthodes que la cible et lui délègue toutes les demandes qu’il reçoit. Il peut exécuter un traitement et modifier le résultat avant ou après avoir envoyé sa demande à la cible.
À quel moment un emballeur devient-il réellement un décorateur ? Comme je l’ai déjà dit, un emballeur implémente la même interface que l’objet emballé. Du point de vue du client, ces objets sont identiques. L’attribut de la référence de l’emballeur doit pouvoir accueillir n’importe quel objet qui implémente cette interface. Vous pouvez ainsi utiliser plusieurs emballeurs sur un seul objet et lui attribuer les comportements de plusieurs emballeurs en même temps.
Reprenons notre exemple et mettons-y une notification par e-mail dans la classe de base Notificateur
, mais transformons toutes les autres méthodes de notification en décorateurs.
Le code client doit emballer un objet basique Notificateur pour le transformer en un ensemble de décorateurs adapté aux préférences du client. Les objets qui en résultent sont empilés.
Le client traite le dernier objet de la pile. Les décorateurs implémentent tous la même interface (le notificateur de base). Le reste du code client manipule indifféremment l’objet notificateur original et l’objet décoré.
Nous pouvons utiliser cette technique pour tous les autres comportements, comme la mise en page des messages ou la création de la liste des destinataires. Le client peut décorer l’objet avec des décorateurs personnalisés tant qu’ils suivent la même interface que les autres.
Analogie
Porter des vêtements est un bon exemple d’utilisation. Si vous avez froid, vous vous enroulez dans un pull. Si vous avez encore froid, vous pouvez porter un blouson par-dessus. S’il pleut, vous enfilez un imperméable. Tous ces vêtements « étendent » votre comportement de base mais ne font pas partie de vous, et vous pouvez facilement enlever un vêtement lorsque vous n’en avez plus besoin.
Structure
-
Le Composant déclare l’interface commune pour les décorateurs et les objets décorés.
-
Le Composant Concret est une classe contenant des objets qui vont être emballés. Il définit le comportement par défaut qui peut être modifié par les décorateurs.
-
Le Décorateur de Base possède un attribut pour référencer un objet emballé. L’attribut doit être déclaré avec le type de l’interface du composant afin de contenir à la fois les composants concrets et les décorateurs. Le décorateur de base délègue toutes les opérations à l’objet emballé.
-
Les Décorateurs Concrets définissent des comportements supplémentaires qui peuvent être ajoutés dynamiquement aux composants. Les décorateurs concrets redéfinissent les méthodes du décorateur de base et exécutent leur traitement avant ou après l’appel à la méthode du parent.
-
Le Client peut emballer les composants dans plusieurs couches de décorateurs, tant qu’il manipule les objets à l’aide de l’interface du composant.
Pseudo-code
Dans cet exemple, le Décorateur permet la compression et le chiffrage des données indépendamment du code qui les utilise.
L’application emballe l’objet de la source de données à l’aide de deux décorateurs. Ils modifient la manière dont les données sont lues et écrites sur le disque :
-
Les décorateurs chiffrent et compressent les données juste avant qu’elles soient écrites sur le disque. La classe d’origine écrit les données chiffrées et protégées dans le fichier sans savoir qu’il y a eu une modification.
-
Une fois que les données sont lues depuis le disque, elles repassent dans ces mêmes décorateurs qui les décompressent et les déchiffrent.
Les décorateurs et la classe de la source de données implémentent la même interface, ce qui les rend interchangeables aux yeux du code client.
Possibilités d’application
Utilisez le décorateur si vous avez besoin d’ajouter des comportements supplémentaires au moment de l’exécution sans avoir à altérer le code source de ces objets.
Le décorateur vous permet de structurer votre logique métier en couches, de créer un décorateur pour chacune de ces couches et de décorer les objets avec différentes combinaisons au moment de l’exécution. Le code client peut traiter les objets uniformément puisqu’ils implémentent la même interface.
Utilisez ce patron si l’héritage est impossible ou peu logique pour étendre le comportement d’un objet.
De nombreux langages de programmation permettent l’utilisation du mot clé final
pour interdire l’héritage d’une classe. Le seul moyen d’étendre le comportement d’une telle classe est de l’emballer en utilisant un décorateur.
Mise en œuvre
-
Assurez-vous que votre domaine peut être représenté sous la forme d’un composant principal recouvert par plusieurs couches facultatives.
-
Déterminez les méthodes communes entre le composant principal et les couches facultatives. Créez l’interface du composant et déclarez-y ces méthodes.
-
Créez une classe concrète pour le composant et définissez son comportement de base.
-
Créez une classe de base décorateur. Elle doit inclure un attribut qui va permettre de stocker la référence à un objet emballé. Cet attribut doit être déclaré avec le type de l’interface du composant, afin de le relier aux composants concrets et aux décorateurs. Le décorateur de base doit déléguer tout le travail à l’objet emballé.
-
Assurez-vous que les classes implémentent l’interface du composant.
-
Créez des décorateurs concrets en les implémentant à partir du décorateur de base. Un décorateur concret doit exécuter son comportement avant ou après l’appel à la méthode de son parent (qui délègue toujours la tâche à l’objet emballé).
-
Le code client doit être responsable de la création des décorateurs et de leur agencement en fonction des besoins du client.
Avantages et inconvénients
- Vous pouvez étendre le comportement d’un objet sans avoir recours à la création d’une nouvelle sous-classe.
- Vous pouvez ajouter ou retirer dynamiquement des responsabilités à un objet au moment de l’exécution.
- Vous pouvez combiner plusieurs comportements en emballant un objet dans plusieurs décorateurs.
- Principe de responsabilité unique. Vous pouvez découper une classe monolithique qui implémente plusieurs comportements différents en plusieurs petits morceaux.
- Retirer un emballeur spécifique de la pile n’est pas chose aisée.
- Il n’est pas non plus aisé de mettre en place un décorateur dont le comportement ne varie pas en fonction de sa position dans la pile.
- Le code de configuration initial des couches peut avoir l’air assez moche.
Liens avec les autres patrons
-
L'Adaptateur fournit une interface complètement différente pour accéder à un objet existant. En revanche, avec le modèle du Décorateur, l’interface reste la même ou est étendue. De plus, Décorateur supporte la composition récursive, ce qui n’est pas possible lorsque vous utilisez Adaptateur.
-
Avec Adaptateur, vous accédez à un objet existant via une interface différente. Avec Procuration, l’interface reste la même. Avec Décorateur, vous accédez à l’objet via une interface améliorée.
-
La Chaîne de Responsabilité et le Décorateur ont des structures de classes très similaires. Ils reposent tous deux sur la composition récursive pour passer les appels à une suite d’objets. Ils possèdent cependant plusieurs différences majeures.
Les handlers de la chaîne de responsabilité peuvent lancer des opérations arbitraires indépendantes l’une de l’autre. Ils peuvent également décider de ne pas propager la demande plus loin dans la chaîne. Les décorateurs quant à eux peuvent étendre le comportement de l’objet sans perturber leur fonctionnement avec l’interface de base. De plus, les décorateurs n’ont pas le droit d’interrompre la demande.
-
Le Composite et le Décorateur ont des diagrammes de structure similaires puisqu’ils reposent sur la composition récursive pour organiser un nombre variable d’objets.
Un décorateur est comme un composite, mais avec un seul composant enfant. Il y a une autre différence importante : Le décorateur ajoute des responsabilités supplémentaires à l’objet emballé, alors que le composite se contente d’« additionner » les résultats de ses enfants.
Mais ces patrons de conception peuvent également coopérer : vous pouvez utiliser le décorateur pour étendre le comportement d’un objet spécifique d’un arbre Composite.
-
Les conceptions qui reposent énormément sur le Composite et le Décorateur tirent des avantages à utiliser le Prototype. Il vous permet de cloner les structures complexes plutôt que de les reconstruire à partir de rien.
-
Le Décorateur vous permet de changer la peau d’un objet, alors que la Stratégie vous permet de changer ses tripes.
-
Le Décorateur et la Procuration ont des structures similaires, mais des intentions différentes. Ces deux patrons sont bâtis sur le principe de la composition, où un objet est censé déléguer certains traitements à un autre. La différence est qu’en principe, la procuration gère elle-même le cycle de vie de son objet service, alors que la composition des décorateurs est toujours contrôlée par le client.