Ruby et la décoration
Où le développeur se découvre une âme artiste…
Le pattern Décorateur
D’après Wikipédia:
Un décorateur permet d’attacher dynamiquement de nouvelles responsabilités à un objet. Les décorateurs offrent une alternative assez souple à l’héritage pour composer de nouvelles fonctionnalités.
Concrètement, mettre en place un pattern décorateur consiste à encapsuler une classe cible dans le décorateur, et à surcharger un comportement spécifique.
Ruby propose de base une manière élégante de répondre à ce genre de besoin (la classe SimpleDelegator), mais nous verrons qu’il est aussi possible de modifier plus ou moins dynamiquement et sans modifier le code client de ma classe cible.
SimpleDelegator
Imaginons que nous ayons le même souci que celui présenté dans la page Wikipédia:
Nous savons qu’une voiture a un prix:
Et nous savons qu’une voiture de la marque Citron vaut 250 € (et qu’elle fait ‘Honk !’):
Comment modéliser de manière non intrusive l’ajout d’options à mon véhicule pour pouvoir calculer le nouveau prix ?
Première implémentation des options
Tout d’abord, nous allons d’abord inclure le fichier delegate:
Ensuite, nous allons déclarer une classe DiscoDecorator, qui ajoute au prix total de la voiture le prix d’un mini boule à facettes (soit 3.50 €):
Dans cette class, nous avons juste écrit la méthode prix, qui appelle prix sur l’objet décoré, et le retourne ajouté de 3.50 €.
Il y a deux choses importantes à savoir sur la classe SimpleDelegator:
- Il faut l’initialiser avec l’objet à décorer. Toutes les méthodes non-surchargées seront appellées sur cet objet.
- La méthode __getobj__ renvoie l’objet décoré.
Ajoutons aussi une classe MoumoutteDecorator, avec une moumoutte de volant à la modique somme de 15 €:
On peut ainsi composer à l’envie une Citron avec option disco (notez le passage de citron au constructeur du décorateur):
Une citron avec une moumoutte de volant:
Ou bien les deux en encapsulant un décorateur dans un autre décorateur:
Et bien entendu, l’objet décoré garde toutes ses autres fonctions intactes:
Implémentation générique des options
On peut aussi imaginer créer une classe générique OptionDecorator, qui va hériter de SimpleDelegator:
Ainsi, l’ajout d’option se ferait à la volée, sans nul besoin d’écrire une classe à chaque fois:
La décoration est un concept simple et puissant qui peut permettre d’enrichir le comportement d’une classe sans en altérer la mécanique interne.
Voici un exemple un peu plus velu:
Ici, on a créé un decorateur paramètrable, dont le constructeur attends un nom d’option, un prix, et l’objet décoré.
On peut l’utiliser pour décorer une citron avec plusieurs options:
Ici, dans la boucle de l’inject, on encapsule la citron dans trois DetailedOptionDecorator. Ensuite, il ne reste plus qu’à appeller prix et afficher son résultat:
Ce code va afficher un détail et le prix total de la voiture avec moumoutte, climatisation et boule disco:
Prix toutes options comprises:
250.00 €
+ 15.00 € (Moumoutte)
+ 105.00 € (Clim)
+ 3.50 € (Disco)
=======================
373.50 €
Et notre voiture décorée fait toujours ‘Honk !’:
L’approche introspective
L’inconvénient de l’utilisation de SimpleDelegator, c’est que les appels internes au fonctionnement de la classe de la fonction décorée ne passent pas par le décorateur.
Si l’on reprend notre classe voiture pour lui ajouter une méthode tva (qui va renvoyer 20% du prix de la voiture:
Une voiture décorée renverra la même tva que la voiture non décorée:
Le remplacement de méthode à la volée
En revanche, il est possible de d’envisager un remplacement à la volée d’une méthode par une méthode dite ‘singleton’ (une méthode qui n’existe que pour la méta-classe de l’instance de l’objet sur laquelle on l’ajoute), mais les inconvénients sont multiples:
- On modifie l’instance de l’objet qu’on souhaite décorer, ce qui viole le principe d’enrichissement non-intrusif du décorateur.
- On ne peut pas décorer deux fois la même instance, sous peine de se manger un ‘stack too deep’.
Pour la blague, voici un exemple d’implémentation:
Module + Extend + Super decorator = Joy
Dan Croak a fait quelques recherches sur le sujet dans un article interessant intitulé Evaluating Alternative Decorator Implementations In Ruby.
Une des méthodes qui a attiré mon attention est celle qu’il intitule ‘Module + Extend + Super decorator’.
Cette fois, les décorateurs sont des modules qui surchargent la fonction à décorer:
Et il suffit d’étendre l’objet avec le module qu’on souhaite utiliser comme décorateur:
Les avantages de cette méthode sont relativement évidents (lisibilité, délégation effectuée en profondeur, etc), mais elle est affublée d’un inconvénient majeur: il est impossible d’appliquer deux fois le même décorateur (sauf à user de sombres magouilles que je n’étalerais en aucun ca ici…).
Conclusion
Ce petit article est loin d’avoir fait le tour du sujet de l’implémentation du pattern décorateur en ruby. J’espère néanmoins qu’il aura eu le mérite de faire découvrir quelque chose à certains d’entre vous.
Pour les plus curieux, il est possible de télécharger le fichier ruby qui a servi à l’élaboration de cet article..
N’hésitez pas à me pinger sur twitter en cas de question ou remarques.