Étendre son JavaScript

Justin Naifeh

Traducteur

vermine

Cet article est la traduction de Decorating Your JavaScript publié sur DailyJS.
Commentez

- Titre : Étendre son JavaScript

- Auteur : Justin Naifeh

- Traducteur : vermine

- Parution : 5 novembre 2012

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2012 Justin Naifeh. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Le problème

- III. Les solutions

- III-A. Réécrire le comportement

- III-B. Changer le code appelant

- III-C. Sous-classe du composant

- III-D. Modifier le code source

- IV. La décoration

- V. L'implémentation

- VI. Les critiques

- VII. Conclusion

- VIII. Remerciements

I - Introduction

Le decorator 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 :

/**
* Author constructeur.
*
* @param String firstName
* @param String lastName
*/
var Author = function(firstName, lastName) {
this._firstName = firstName;
this._lastName = lastName;
};

Author.prototype = {

/**
* @return String Le firstName de l'auteur.
*/
getFirstName: function() {
return this._firstName;
},

/**
* @return String Le lastName de l'auteur.
*/
getLastName: function() {
return this._lastName;
},

/**
* @return String La représentation d'un auteur.
*/
toString: function() {
return this.getFirstName()+' '+this.getLastName();
}
};

/**
* Publication constructeur.
*
* @param String title
* @param Author[] authors
* @param int type
*/
var Publication = function(title, authors, type) {
this._title = title;
this._authors = authors;
this._type = type;
};

Publication.prototype = {

/**
* @return String Le titre de la publication.
*/
getTitle: function() {
return this._title;
},

/**
* @return Author[] Tous les auteurs.
*/
getAuthors: function() {
return this._authors;
},

/**
* @return int Le type de la publication.
*/
getType: function() {
return this._type;
},

/**
* Une publication peut avoir plusieurs auteurs,
* mais on s'intéresse uniquement aux trois premiers.
*
* @return Author[] Les principaux contributeurs.
*/
contributingAuthors: function() {
return this._authors.slice(0, 3);
},

/**
* @return String La représentation d'une publication.
*/
toString: function() {
return '['+this.getType()+'] "'+this.getTitle()+'" par '+this.contributingAuthors().join(', ');
}
};

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é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 instanceof 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 composition par rapport à l'héritage pour éviter la dépendance à la classe de base (pour les développeurs masochistes).

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 polymorphisme 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é ».

/**
* MedicalPublication constructeur.
*
* @param PublicationIF La publication à décorer.
*/
var MedicalPublication = function(publication) {
this._publication = publication;
};

MedicalPublication.prototype = {

/**
* @return String Le titre de la publication.
*/
getTitle:  function() {
return this._publication.getTitle();
},

/**
* @return Author[] Tous les auteurs.
*/
getAuthors: function() {
return this._publication.getAuthors();
},

/**
* @return int Le type de la publication.
*/
getType: function() {
return this._publication.getType();
},

/**
* Retourne le premier et dernier auteurs si plusieurs auteurs existent
* Sinon, le premier auteur est retourné. C'est une convention dans le
* domaine des publications médicales.
*
* @return Author[] Les principaux contributeurs.
*/
contributingAuthors: function() {

var authors = this.getAuthors();

if (authors.length > 1) {
// cherche le premier et le dernier contributeurs
return authors.slice(0, 1).concat(authors.slice(-1));
} else {
// zéro ou un contributeur
return authors.slice(0, 1);
}
},

/**
* @return String La représentation de la publication.
*/
toString: function() {
return 'Decorated - ['+this.getType()+'] "'+this.getTitle()+'" par '+this.contributingAuthors().join(', ');
}
};

/**
* Méthode factory pour instancier l'implémentation appropriée de PublicationIF.
*
* @param String Le type permettant de sélectionner une implémentation.
* @param String Le titre de la publication.
* @param Author[] Les auteurs de la publication.
* @return L'objet PublicationIF créé.
*/
var publicationFactory = function(title, authors, type) {

if (type === 'médical') {
return new MedicalPublication(new Publication(title, authors, type));
} else {
return new Publication(type, title, authors);
}
};

À 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 principes 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.