I. Introduction▲
Le decorator patterndecorator pattern, également connu sous le nom de wrapper, est un mécanisme permettant d'étendre le comportement d'exécution d'un objet, un processus connu comme étant une décoration. Le modèle est souvent négligé car sa simplicité dément ses prestations orientées objet lorsque vous écrivez du code modulable. Les objets étendus sont également délaissés par le JavaScript car la nature dynamique du langage permet aux développeurs d'abuser de la malléabilité des objets. Parce que "vous pouvez" ne signifie pas "vous devrez".
Avant de plonger dans le decorator pattern, nous allons examiner un problème de codage réaliste qui peut être résolu avec d'autres solutions. On comprend mieux la décoration une fois que les défauts des autres solutions communes ont été explorées.
II. Le problème▲
Vous écrivez un simple outil d'archivage qui gère l'affichage et le cycle de vie des publications et de leurs auteurs. Une caractéristique importante est la possibilité de lister les contributions des auteurs, qui peuvent être un sous-ensemble de tous les auteurs. La valeur par défaut est de montrer les trois premiers auteurs de toutes les publications. Le modèle de domaine initial est simpliste :
À l'aide de JavaScript simple, nous implémentons les classes en lecture seule comme suit :
L'API est simple. Considérez les invocations suivantes :
var pub =
new Publication
(
'The Shining'
,
[
new Author
(
'Stephen'
,
'King'
)],
'horreur'
);
// s'appuient sur la valeur par défaut toString() pour afficher : [horreur] "The Shining" par Stephen King
alert
(
pub);
// ...
var pub2 =
new Publication
(
'Design Patterns: Elements of Reusable Object-Oriented Software'
,
[
new Author
(
'Erich'
,
'Gamma'
),
new Author
(
'Richard'
,
'Helm'
),
new Author
(
'Ralph'
,
'Johnson'
),
new Author
(
'John'
,
'Vlissides'
)
],
'programmation'
);
// affiche : [programmation] "Design Patterns: Elements of Reusable Object-Oriented Software"
// par Erich Gamma, Richard Helm, Ralph Johnson
alert
(
pub2);
Le design est simple et fiable... jusqu'à ce que le client spécifie une nouvelle exigence :
Conformément à la convention pour les publications médicalespublications médicales, lister uniquement le premier (primaire) et dernier (superviseur) auteurs s'il existe plusieurs auteurs.
Cela signifie que si Publication.getType() retourne « médical » nous devons exécuter une logique spéciale pour obtenir la liste des auteurs. Tous les autres types (par exemple, horreur, romance, ordinateur, etc.) utilise le comportement par défaut.
III. Les solutions▲
Il existe plusieurs solutions pour satisfaire aux nouvelles exigences, mais certaines ont des inconvénients qui ne sont pas immédiatement apparents. Nous allons explorer quelques-unes d'entre elles et voir pourquoi elles ne sont pas idéales même si elles sont monnaie courante.
III-A. Réécrire le comportement▲
// réécrire la définition de contributingAuthors
if (
pub.getType
(
) ===
'médical'
) {
pub.
contributingAuthors =
function(
) {
var authors =
this.getAuthors
(
);
// retourne le premier et dernier auteurs si possible
if (
authors.
length >
1
) {
return authors.slice
(
0
,
1
).concat
(
authors.slice
(-
1
));
}
else {
return authors.slice
(
0
,
1
);
}
}
}
Cela, on pourrait le faire valoir, peut être la caractéristique la plus maltraitée du langage : la capacité de remplacer arbitrairement les propriétés et le comportement au moment de l'exécution. Maintenant la condition if/else doit être maintenue et élargie si des exigences supplémentaires sont ajoutées pour spécifier les auteurs. En outre, on peut se demander ou non si pub est toujours une instance de Publication. Une vérification rapide de instanceofinstanceof confirmera qu'il l'est, mais une classe définit un ensemble d'états et de comportements. Dans ce cas, nous avons modifié certaines instances et le code appelant ne peut plus certifier la cohérence des objets Publication.
III-B. Changer le code appelant▲
var listing;
if (
pub.getType
(
) ===
'médical'
) {
var contribs =
pub.getAuthors
(
);
// retourne le premier et dernier auteurs si possible
if (
contribs.
length >
1
) {
contribs =
contribs.slice
(
0
,
1
).concat
(
contribs.slice
(-
1
));
}
else {
contribs =
contribs.slice
(
0
,
1
);
}
listing =
'['
+
pub.getType
(
)+
'] "'
+
pub.getTitle
(
)+
'" par '
+
contribs.join
(
', '
);
}
else {
listing =
pub.toString
(
);
}
alert
(
listing);
Cette solution va à l'encontre de l'encapsulation en forçant le code appelant à comprendre l'implémentation interne de Publication.toString() et à la recréer à l'extérieur de la classe. Un bon design ne doit pas être un fardeau pour le code appelant.
III-C. Sous-classe du composant▲
Une des solutions les plus courantes consiste à créer une classe MedicalPublication qui étend Publication, avec une réécriture de contributingAuthors() pour fournir un comportement personnalisé. Bien que cette approche est sans doute moins imparfaite que les deux premières, elle pousse la limite de l'héritage propre. Nous devrions toujours favoriser la compositionComposition over inheritance par rapport à l'héritage pour éviter la dépendance à la classe de baseFragile base class (pour les développeurs masochistesA Study of The Fragile Base Class Problem).
Le "sous-classement" échoue également en tant que stratégie viable lorsque plus d'une personnalisation pourrait survenir ou lorsqu'il y a une combinaison inconnue des personnalisations. Un exemple souvent cité est un logiciel pour modéliser un café-restaurant où les clients peuvent personnaliser leur tasse de café, ce qui affecte le prix. Un développeur peut créer des sous-classes qui reflètent la myriade de combinaisons telles que CoffeeWithCream et CoffeeWithoutCreamExtraSugar qui remplacent les Coffee.getPrice(), mais il est facile de voir que la conception n'évoluera pas.
III-D. Modifier le code source▲
contributingAuthors
:
function(
) {
if (
this.getType
(
) ===
'médical'
) {
var authors =
this.getAuthors
(
);
if (
authors.
length >
1
) {
return authors.slice
(
0
,
1
).concat
(
authors.slice
(-
1
));
}
else {
return authors.slice
(
0
,
1
);
}
}
else {
return this.
_authors.slice
(
0
,
3
);
}
}
C'est un peu un détournement mais, dans un petit projet où vous contrôlez le code source, cette technique pourrait suffire. Un inconvénient évident est que la condition if/else doit croître avec chaque comportement personnalisé, ce qui en fait un cauchemar de maintenance éventuelle.
Une autre chose à noter est que vous ne devez jamais, mais vraiment jamais modifier le code source en dehors de votre contrôle. Même la mention d'une telle idée doit laisser un goût dans votre bouche pire que celui de boire un jus d'orange après le brossage des dents. Faire cela fera inextricablement coupler votre code à cette révision de l'API. Les cas où c'est une option valide sont tellement rares qu'ils représentent habituellement un problème architectural dans l'application, et non dans le code externe.
IV. La décoration▲
Ces solutions s'acquittent de l'exigence relative au coût de mettre en péril la maintenabilité et l'évolutivité. En tant que développeur, vous devez choisir ce qui convient à votre application, mais il y a plus d'une option à examiner avant de prendre une décision.
Je recommande d'utiliser la décoration, un modèle souple permettant d'étendre le comportement de vos objets existants. L'UML suivant représente une implémentation abstraite du modèle :
Les classes ConcreteComponent et Decorator implémentent la même interface Component (ou étendent Component s'il s'agit d'une super classe). Decorator conserve une référence à un Component pour la délégation, sauf dans le cas où nous « décorons » en personnalisant le comportement.
En adhérant au contrat de Component, nous garantissons une API cohérente et protectrice contre les implémentations internes parce que le code appelant ne saura pas et ne doit pas savoir si l'objet est un ConcreteComponent ou un Decorator. La programmation de l'interface est la pierre angulaire d'une bonne conception orientée objet.
Certains prétendent que le JavaScript n'est pas orienté objet alors qu'il prend en charge le prototypage au lieu des classes. Les objets sont toujours innés au langage. Le langage prend en charge le polymorphismePolymorphism in object-oriented programming et le fait que tous les objets étendent Object suffit à faire valoir que le langage est orienté objet ainsi que fonctionnel.
V. L'implémentation▲
Notre solution utilise une légère variante du decorator pattern car le JavaScript n'a pas de notion d'héritage classique comme des interfaces ou des classes abstraites. Il existe de nombreuses bibliothèques qui simulent ces constructions, ce qui est bénéfique pour certaines applications, mais ici nous allons utiliser les rudiments des langages.
Les classes MedicalPublication et Publication implémentent implicitement PublicationIF. Dans ce cas, MedicalPublication agit comme la décoration pour répertorier le premier et dernier auteurs comme contributeurs sans changer les autres comportements.
Notez que MedicalPublication fait référence à PublicationIF et non à Publication. En faisant référence à l'interface au lieu d'une implémentation spécifique nous pouvons arbitrairement agencer les décorations les unes dans les autres (dans le problème du café-restaurant, nous pouvons créer des décorations telles que WithCream, WithoutCream et ExtraSugar, lesquelles peuvent être imbriquées pour gérer n'importe quel ordre complexe) !
La classe MedicalPublication délègue toutes les opérations standards et réécrit contributingAuthors() pour fournir le comportement « décoré ».
À l'aide de la méthode factory, nous pouvons en toute sécurité créer une instance de PublicationIF.
var title =
'Pancreatic Extracts as a Treatment for Diabetes'
;
var authors =
[
new Author
(
'Adam'
,
'Thompson'
),
new Author
(
'Robert'
,
'Grace'
),
new Author
(
'Sarah'
,
'Townsend'
)];
var type =
'médical'
;
var pub =
publicationFactory
(
title,
authors,
type);
// affiche : Decorated - [medical] 'Pancreatic Extracts as a Treatment of Diabetes' par Adam Thompson, Sarah Townsend
alert
(
pub);
Dans ces exemples, nous utilisons toString() pour la brièveté et le débogage, mais maintenant nous pouvons créer des classes et des méthodes utilitaires pour afficher des objets PublicationIF.
Une fois que l'application est modifiée pour obtenir des objets PublicationIF, nous pouvons gérer des exigences supplémentaires, ce qui constitue un important travail en ajoutant de nouvelles décorations. En outre, la conception est maintenant ouverte pour les implémentations de PublicationIF au-delà des décorations afin de remplir d'autres exigences, ce qui augmente considérablement la flexibilité du code.
VI. Les critiques▲
Une critique est que la décoration doit être maintenue pour adhérer à son interface. Tout le code, quelle que soit la conception, doit être maintenu dans une certaine mesure, mais on peut faire valoir que le maintien d'une conception avec un contrat clairement défini avec des pré et post conditions est beaucoup plus simple que la recherche de conditions if/else pour l'état d'exécution et les modifications de comportement. Plus important encore, le decorator pattern protège le code appelant rédigé par d'autres développeurs (ou vous-même) en tirant parti des principesOpen/closed principle orientés objet.
Une autre critique est que les décorations doivent implémenter toutes les opérations définies par un contrat pour faire respecter une API cohérente. Tandis que ceci peut être fastidieux à certains moments, il y a les bibliothèques et les méthodologies qui peuvent être utilisées avec la nature dynamique du JavaScript pour accélérer le codage. Les réflexions de type invocation permettent d'apaiser les inquiétudes lorsqu'on traite avec une API changeante.
/**
* Invoque la méthode cible et se repose sur ses pre et post conditions.
*/
Decorator.
prototype.
someOperation =
function(
) {
return this.
_decorated.
someOperation.apply
(
this.
_decorated,
arguments);
};
// ... ou une bibliothèque qui gère automatiquement cette fonction
/**
* Invocation dynamique.
*
*
@param
Class La classe qui définit la fonction.
*
@param
String La fonction à éxécuter.
*
@param
Object Le contexte d'éxécution *this*.
*/
function wrapper
(
klass,
func,
context) {
return function(
) {
return klass.
prototype[
func]
.apply
(
context,
arguments);
};
};
Les détails sont à la hauteur du développeur, mais même le plus primitif decorator pattern est extrêmement puissant. Les frais généraux et la maintenance pour le modèle lui-même est minime, surtout si on les compare à ceux des solutions opposées.
VII. Conclusion▲
Le decorator pattern n'est pas flashy, malgré son nom, et ne donne pas au développeur la possibilité de dire dans son département « Regardez ce que j'ai fait ! », à savoir les droits de vantardise. Ce que la décoration fait, toutefois, est de correctement encapsuler et modulariser votre code pour le rendre plus évolutif pour les changements à venir. Lorsqu'une nouvelle exigence affirme qu'un certain type de publication doit répertorier tous les auteurs comme collaborateurs, quel que soit le rang ordinal, vous ne vous inquiétez pas d'avoir à refactoriser des centaines de lignes de code. Au lieu de cela, vous écrirez une nouvelle décoration et la déposerez dans la méthode factory. Il vous reste alors à prendre un déjeuner extra long car vous le méritez.
VIII. Remerciements▲
Cet article a été publié avec l'aimable autorisation de Justin Naifeh pour l'article original Decorating Your JavaScript publié sur le site DailyJS.
Je remercie également FirePrawn pour sa relecture attentive.