Article écrit par Tom Panier
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 !
Un exemple pas piqué des vers
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.
Des plugins un peu plus évolués
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.
Les promesses, c’est sacré !
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 😉
And voilà
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 !