Cet article est publié sous licence CC BY-NC-SA
Salutations ! Aujourd’hui, je vous propose un article un poil plus « avancé » puisqu’il s’adresse davantage aux auteur·rice·s de bibliothèques ; ce pourrait être le premier d’une série plus ou moins longue, alors n’hésitez pas à nous donner votre avis en commentaire !
Nous allons étudier ensemble un pattern qui peut s’avérer fort utile dans ce contexte, à savoir l’écriture d’une bibliothèque supportant un système de plugins. Trêve de palabres, plongeons en triple salto dans le vif du sujet !
Par souci de simplicité, nous allons prendre un exemple… simple, à savoir une bibliothèque permettant d’appliquer une série de traitements sur une chaîne de caractères. Toujours pour la même raison, notre code consistera en un simple fichier JavaScript que nous exécuterons avec Node.js via la commande suivante :
$ node path/to/file.js
Chacun des plugins sera représenté par une simple fonction prenant comme unique argument la valeur d’origine et retournant le résultat de son traitement ; commençons par en écrire quelques-uns :
const capitalizePlugin = input => input.split(" ").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
const moreLovePlugin = input => input.replace("aime", "adore");
Passons maintenant au cœur du système, à savoir la fonction chargée d’appliquer à une chaîne les différents traitements d’un ensemble donné de plugins :
function processWithPlugins(input, plugins) {
let output = input;
plugins.forEach(plugin => {
output = plugin(output);
});
return output;
}
Attends, on itère sur les valeurs d’un tableau pour construire un résultat unique… ça me dit quelque chose !
En effet ! Si vous avez lu mon dernier article,
vous devriez avoir deviné que c’est un cas d’utilisation parfait pour
Array.prototype.reduce
:
function processWithPlugins(input, plugins) {
return plugins.reduce((output, plugin) => plugin(output), input);
}
Voilà qui est plus concis ! Testons donc ce que nous avons écrit jusqu’ici :
console.log(processWithPlugins("J'aime les frites !", [capitalizePlugin, moreLovePlugin]));
J'adore Les Frites !
Les deux traitements ont bien été appliqués successivement, dans l’ordre de leur position dans le tableau.
Ce que nous avons pour l’instant fonctionne bien, mais il serait pertinent de
pouvoir faire en sorte que chaque traitement ne soit appliqué que sous
certaines conditions, afin d’éviter par exemple d’appliquer inutilement
capitalize
sur une chaîne où tous les mots commencent déjà par autre chose
qu’une lettre minuscule.
Nous allons donc réécrire nos plugins en en faisant cette fois-ci des objets
comportant deux méthodes : une méthode process
effectuant le traitement comme
précédemment, et une méthode shouldProcess
retournant un booléen indiquant,
en fonction de la valeur d’entrée, si ce traitement doit être appliqué ou non :
const capitalizePlugin = {
process(input) {
return input.split(" ").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
},
shouldProcess(input) {
return !!input.match(/\b[a-z]/); // y a-t-il au moins un mot commençant par une minuscule ?
}
};
const moreLovePlugin = {
process(input) {
return input.replace("aime", "adore");
},
shouldProcess(input) {
return true; // on a toujours besoin de plus d'amour
}
};
Modifions également notre fonction processWithPlugins
afin de tenir compte du
résultat de shouldProcess
:
function processWithPlugins(input, plugins) {
return plugins.reduce((output, plugin) => plugin.shouldProcess(output)
? plugin.process(output)
: output
, input);
}
Il est dès lors facile de valider le fonctionnement de ce nouveau pattern, en
ajoutant par exemple un log dans le shouldProcess
de capitalizePlugin
:
const capitalizePlugin = {
process(input) {
return input.split(" ").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
},
shouldProcess(input) {
const result = !!input.match(/\b[a-z]/);
console.log(input, result);
return result;
}
};
console.log(processWithPlugins("J'aime les frites !", [capitalizePlugin, moreLovePlugin]));
console.log(processWithPlugins("I'M ALL CAPS", [capitalizePlugin, moreLovePlugin]));
J'aime les frites ! true
J'adore Les Frites !
I'M ALL CAPS false
I'M ALL CAPS
Notez qu’il est quelque peu dommage de devoir déclarer une fonction
shouldProcess
pour moreLovePlugin
alors que nous savons qu’il s’exécutera
systématiquement ; peut-être pouvons-nous nous en passer ?
const moreLovePlugin = {
process(input) {
return input.replace("aime", "adore");
}
};
function processWithPlugins(input, plugins) {
return plugins.reduce((output, plugin) => !("shouldProcess" in plugin) || plugin.shouldProcess(output)
? plugin.process(output)
: output
, input);
}
Bingo ! Cette version considérera que l’absence de shouldProcess
équivaut à
une implémentation retournant toujours true
. En s’amusant avec typeof
, on
pourrait aussi imaginer supporter également les plugins exclusivement
constitués d’une fonction, telles les premières moutures des nôtres.
Pour cette dernière partie, nous allons voir comment modifier notre code afin
de supporter les plugins réalisant un traitement asynchrone, ou en d’autres
termes, retournant une Promise
qui sera résolue à la valeur traitée.
Installons donc une nouvelle dépendance :
$ npm i node-fetch
Et ajoutons dans la foulée un tel plugin :
const weekendPlugin = {
process(input) {
return fetch("https://estcequecestbientotleweekend.fr/")
.then(r => r.text())
.then(body => body.replace(/\n/g, "").match(/\<p class="msg"\>([^<]+)\<\/p\>/)[1].trim())
.then(msg => input + " Est-ce que c'est bientôt le week-end ? " + msg);
}
};
Il nous faut ici prêter attention à un détail important : afin de pouvoir
traiter toute notre chaîne de plugins en faisant abstraction de leur
synchronicité, nous devons modifier processWithPlugins
afin de lui faire
gérer la résolution des Promise
s, ainsi que le wrapping du résultat de
l’exécution de chacun d’eux dans une Promise
. Nous manipulerons ainsi des
Promise
s (vous non plus, vous n’avez jamais lu ce mot autant de fois
d’affilée ?) exclusivement :
function processWithPlugins(input, plugins) {
return plugins.reduce((output, plugin) => Promise.resolve(output).then(output => new Promise(resolve => resolve(
!("shouldProcess" in plugin) || plugin.shouldProcess(output) ? plugin.process(output) : output
))), input);
}
Cette fonction étant, dans les faits, devenue asynchrone, nous devons également
impacter le code appelant (et en profiter pour ajouter weekendPlugin
à la
liste) :
processWithPlugins("J'aime les frites !", [capitalizePlugin, moreLovePlugin, weekendPlugin]).then(console.log);
J'adore Les Frites ! Est-ce que c'est bientôt le week-end ? Non.
Notez que, fort logiquement, le résultat peut varier si vous avez la chance d’exécuter ce code alors que le week-end approche ;)
C’en est fini de cet article ! J’espère qu’il vous a plu et que vous appréciez ce type de considérations algorithmiques ; le cas échéant, comme je le disais en début d’article, n’hésitez pas à le manifester dans les commentaires pour que je vous en propose d’autres du même acabit !
L’équipe Synbioz.
Libres d’être ensemble.
Nos conseils et ressources pour vos développements produit.