JavaScript étant très permissif, il est possible d'écrire à peu près n'importe quoi et de faire fonctionner ce code à peu près correctement.
J'ai eu l'occasion de le voir souvent, sans que ce soit volontaire. Soit c'est par manque de temps, soit c'est par incompétence.
Comment se forcer à écrire du code robuste même par manque de temps ? (Pour l'incompétence, seule une remise en cause vous sauvera)
Non, je ne vais pas parler ici de tests ou de linter.
Pour un code robuste : éviter les mutations
Une des principales causes de régression ou d'introduction de bug est la présence de mutations.
Par exemple :
const obj = {
type: "page",
view: 0,
};
doSomethingAsyncWithObj(obj) // obj sera muté avec increment !
function increment(obj) {
obj.view++; // cette instruction mute l'objet passé en paramètre
return obj;
}
const obj2 = increment(obj);
assert.strictEqual(obj, obj2);
Ici, increment mute l'objet passé en paramètre, ce qui peut provoquer des effets de bord gênants. Comme dans l'exemple, doSomethingAsyncWithObj utilisera l'objet muté.
Pour éviter cela, il faut créer un nouvel objet à chaque fois :
const obj = {
type: "page",
view: 0,
};
doSomethingAsyncWithObj(obj) // obj non muté
function increment(obj) {
return { ...obj, view: obj.view + 1 }; // les propriétés de 1er niveau sont clonées
}
const obj2 = increment(obj);
assert.notEqual(obj, obj2)
Le code pur
La notion de fonction pure, ou code pur, est assez similaire à celle de la non-mutation.
Une fonction pure retourne toujours le même résultat dès lors qu'elle est appelée avec les mêmes paramètres, elle est idempotente.
En JavaScript, la notion de fonction pure et impure est assez simple à visualiser.
Math.random(); // impure
Date.now(); // impure
const add = (a, b) => a + b; // pure
const n = 1;
const addN = (a) => a + n; // impure
Prenons un hook React :
const useIncrement = (value) => {
const increment = () => value + 1
return { increment };
}
Ce code est impur car lors de l'appel à increment, son résultat dépendra de la valeur passée dans useIncrement.
Pour remédier à cela, il suffit de :
const useIncrement = () => {
const increment = (value) => value + 1 // pur
return { increment };
}
Il faut toujours travailler avec un code pur, dans le cas où cela est difficile (Math.random ou Date.now), il faut les isoler dans un utilitaire. L'objectif est de pouvoir les identifier et de pouvoir les mocker pour des tests.
SRP (Single Responsibility Principle)
Le principe de responsabilité unique est un concept essentiel pour développer du code robuste. Chaque élément doit avoir une seule responsabilité.
Il s'agit du S de SOLID (Single responsibility, Open-closed, Liskov substitution, Interface segregation, Dependency inversion).
Voici un exemple de violation de ce principe :
const sendEmail = (user, message) => {
// envoi d'email
// log
// sauvegarde en base
}
La fonction sendEmail fait beaucoup plus d'actions qu'un simple envoi d'email, elle a donc plusieurs responsabilités.
Il faut donc découper cette fonction en plusieurs fonctions :
const sendEmail = (user, message) => {
// envoi d'email
}
const log = (user, message) => {
// log
}
const save = (user, message) => {
// sauvegarde en base
}
Cette séparation évite la duplication de code et facilite les tests unitaires.
Les bases avant le reste
Pour parler de code robuste, la plupart des développeurs discutent de design pattern, de linter ou de tests unitaires. Pourtant, la conception d'un code robuste commence par la compréhension de principes de base.