Qu'est-ce que le refactoring ?
Distinguer refactoring et simple modification de code : améliorer la structure sans changer le comportement, sous la protection des tests.
Le mot « refactoring » est partout. On l'emploie pour dire qu'on a nettoyé un fichier, ajouté une fonctionnalité, changé une bibliothèque, ou réécrit un module en entier. Dans son livre Refactoring JavaScript, Evan Burchard refuse cette acception molle. Pour lui, le refactoring désigne une chose précise et restreinte : améliorer la structure interne du code sans changer son comportement observable. Tout le reste — corriger un bug, ajouter une feature, optimiser — relève d'une autre catégorie, qu'il appelle simplement « changer le code » (changing code).
Cette distinction n'est pas un caprice de vocabulaire. Elle commande toute la méthode du livre. Car on ne peut affirmer qu'un comportement n'a pas bougé que si l'on dispose d'un moyen de le vérifier : un filet de sécurité fait de tests. Sans ce filet, restructurer du code n'est pas du refactoring, c'est du bricolage à l'aveugle. Ce chapitre pose cette frontière, explique pourquoi elle compte, et montre, sur de petits exemples JavaScript, à quoi ressemble une transformation qui ne change rien — et tout à la fois.
La définition stricte de Burchard
Le refactoring, c'est changer du code sans changer le comportement. Le but est d'améliorer la qualité — lisibilité, maintenabilité, extensibilité — tout en préservant ce que le programme fait du point de vue de celui qui l'utilise.
Burchard prend soin d'opposer deux usages du mot, qu'on pourrait noter « Refactoring » (sa définition : restructurer en préservant le comportement, avec confiance) et « refactoring » (l'usage courant, qui veut juste dire « toucher au code »). Le livre n'emploie jamais le second. Mais dans la vraie vie, quand quelqu'un dit « j'ai refactoré le login », il vaut la peine de se demander lequel des deux il a réellement fait : restructurer, ou modifier le comportement.
Note
Burchard s'inscrit dans la lignée de Martin Fowler (Refactoring: Improving the Design of Existing Code) et des pionniers Opdyke et Johnson. Mais là où Fowler insiste sur les « mécaniques » (des séquences de micro-étapes minimisant les états instables) et où l'approche d'Opdyke vise l'automatisation par outillage, Burchard parie sur autre chose : le cycle rouge-vert-refactor appuyé sur des tests, et le contrôle de version pour annuler vite quand ça tourne mal.
Refactoring contre « changer le code »
Voici le critère opérationnel qui sépare les deux. Pour du code existant, toute modification de l'interface (c'est-à-dire du comportement) devrait casser des tests. Si vous changez le comportement et qu'aucun test ne tombe au rouge, c'est le signe d'une couverture insuffisante. À l'inverse, toute modification des seuls détails d'implémentation ne devrait casser aucun test.
Burchard donne une liste explicite de choses qui ne sont pas du refactoring, parce qu'elles créent du code ou des fonctionnalités nouvelles :
- Ajouter une fonction « racine carrée » à une calculatrice.
- Créer une application de zéro.
- Reconstruire une application existante dans un nouveau framework.
- Ajouter un paquet (package) à un projet.
- Saluer un utilisateur par son prénom et son nom au lieu du seul prénom.
- Localiser (traduire) une application.
- Optimiser les performances.
- Convertir une interface synchrone en asynchrone, ou des callbacks en promesses.
Toutes ces tâches modifient le comportement observable. Elles sont souvent plus visibles pour un chef de produit que la qualité du code, et plus directement liées aux objectifs métier. Mais ce sont des changements de comportement, donc distincts du refactoring.
À retenir
Le critère décisif n'est pas « le code a-t-il changé ? » (il change dans les deux cas) mais « le comportement observable a-t-il changé ? ». Refactoring : la structure change, le comportement est préservé. Changer le code : le comportement change, c'est tout l'intérêt.
Pourquoi les détails d'implémentation ne comptent pas
Pour comprendre la frontière, il faut admettre qu'on ne se soucie pas de la manière dont une fonction obtient son résultat, seulement de la correspondance entre ses entrées et ses sorties. Burchard l'illustre avec une fonction qui double un nombre.
// ❌ Avant
function byTwo(number) {
return number * 2;
}
// ✅ Après (même comportement, détail d'implémentation différent)
function byTwo(number) {
return number + number;
} Les deux versions sont interchangeables. Les tests de byTwo ne sont, au fond, qu'une table de correspondance entre un nombre en entrée et son double en sortie. Que l'on écrive * 2, + number ou même << 1 (décalage de bits) relève du détail d'implémentation : un comportement, certes, mais un comportement insignifiant tant que seules les entrées et les sorties nous importent.
Attention toutefois au piège que Burchard tend lui-même. La version par décalage number << 1 casse pour les grands nombres (essayez avec mille milliards : 1000000000000 << 1).
// ❌ Après refactoring imprudent : casse sur les grands nombres
function byTwo(number) {
return number << 1; // 1000000000000 << 1 donne un résultat faux
} Est-ce que ce bug signifie qu'on se met soudain à se soucier du détail << ? Non. On se soucie de ce que la sortie est fausse. La leçon est ailleurs : notre suite de tests devait inclure plus de cas qu'on ne le pensait — notamment les très grands nombres. Une fois ces cas couverts, on échange librement l'implémentation contre n'importe quelle autre qui passe tous les tests. C'est exactement ce que le filet de tests autorise.
Astuce
Le mantra : « écrire pour les humains d'abord » (write for humans first). Tester des détails d'implémentation trop spécifiques (l'opérateur exact, le nombre d'itérations) ne teste plus votre programme mais l'environnement lui-même — et débouche sur une base de code qu'on ne peut plus refactorer librement.
Le filet de sécurité : sans tests, ce n'est pas du refactoring
C'est le cœur de la thèse de Burchard. Le degré auquel on spécifie et teste son code est précisément la mesure du soin qu'on porte à son comportement. Du code sans test, sans procédure d'exécution manuelle, sans même une description de son fonctionnement attendu, est tout simplement invérifiable.
Prenons une fonction opaque, sans aucun test ni documentation :
function doesThings(args, callback) {
doesOtherThings(args);
doesOtherOtherThings(args, callback);
return 5;
} Se soucie-t-on que son comportement change ? Oui, énormément ! Cette fonction tient peut-être tout l'édifice debout. Le fait qu'on ne la comprenne pas ne la rend pas moins importante — cela la rend seulement plus dangereuse. Mais dans le contexte du refactoring, on ne la refactorera pas, justement parce qu'on serait incapable de vérifier que son comportement reste identique.
La règle est sans appel : du code sans test (automatisé ou manuel) ne peut pas être refactoré. Et cela ne concerne pas que le code hérité (legacy) : il est tout aussi impossible de refactorer du code neuf s'il n'est pas couvert. Pour le code legacy non testé, Burchard renvoie à une technique vue plus loin dans le livre, les tests de caractérisation (characterization tests), qui consistent à capturer le comportement actuel avant d'oser y toucher.
Burchard met en scène le dialogue que devrait provoquer toute affirmation de refactoring tant que les tests n'existent pas :
— « J'ai refactoré le login pour accepter l'email et le pseudo. »
— « Non, tu ne l'as pas fait. »
— « Je suis en train de refactorer le code pour ____ »
— « Non. »
— « Avant d'ajouter des tests, il faut refactorer. »
— « Non. » La dernière réplique vise un sophisme courant : « il faut d'abord refactorer pour pouvoir tester ». Non : tant qu'il n'y a pas de filet, restructurer reste un changement de code à risque, pas un refactoring.
Attention
Toute forme de modification de code fait courir un risque sérieux à votre base si vous ne pouvez pas revenir facilement à une version saine. Utilisez le contrôle de version (Git, sauvegardé sur un hébergeur distant). Si votre code n'est pas versionné, c'est la première chose à régler — avant même de penser à refactorer. Le couple « tests + Git » est ce qui rend l'opération sûre : les tests prouvent que le comportement tient, Git permet d'annuler en un instant si une étape échoue.
Performance : un souci secondaire
Une conséquence directe de la définition : au départ, la performance ne nous concerne pas. Comme pour la fonction qui double un nombre, ce qui compte c'est que les entrées produisent les sorties attendues. Une première implémentation est « assez bonne » si elle permet, en un temps raisonnable, de vérifier cette correspondance.
Si l'implémentation est trop lente pour seulement tester les entrées/sorties, alors oui, il faut la changer — mais à ce moment-là, les tests fonctionnels sont en place et fournissent la confiance nécessaire. La performance peut elle-même devenir une exigence vérifiable : par étalonnage (benchmarking), on écrit des tests qui échouent quand une fonction est trop lente et passent quand elle est assez rapide (l'exécution devient l'« entrée », le temps consommé devient la « sortie »). Tant qu'elle n'est pas ainsi placée sous test, la performance n'est pas un « comportement » dont on se soucie de la préservation.
Piège courant
Pour certains types de JavaScript, il est parfois impossible de refactorer sans changer la performance : ajouter quelques lignes de code frontend non minifiées allonge le téléchargement ; les outils de build et compilateurs peuvent réagir à la structure de votre code de mille manières. Ces effets subtils et difficiles à contrôler doivent être traités comme une préoccupation séparée — la promesse « le comportement ne change pas » ne s'y applique pas.
Le but : améliorer la qualité et préparer le changement
Si le comportement ne change pas, à quoi bon refactorer ? Pour améliorer la qualité. Pour Burchard, du code de qualité est du code qui fonctionne correctement et qu'on peut étendre facilement. De là découlent deux préoccupations tactiques : écrire des tests, et écrire du code facilement testable.
Il propose, avec une fausse modestie assumée, ses « principes EVAN » de qualité — un acronyme qu'on peut s'amuser à remplacer par le sien :
- Extract : extraire fonctions et modules pour simplifier les interfaces.
- Verify : vérifier le comportement du code par des tests.
- Avoid : éviter les fonctions impures (impure functions) quand c'est possible.
- Name : bien nommer variables et fonctions.
Mais le bénéfice le plus profond du refactoring est de préparer un changement futur. Votre première intuition de code est rarement optimale : avec autant d'outils et de problèmes différents, exiger de soi de n'écrire que la meilleure solution du premier coup, et de ne jamais y revenir, est totalement irréaliste. Le refactoring offre la liberté inverse : écrire d'abord sa meilleure approximation du code et du test, puis, sous la protection des tests qui figent l'interface, faire évoluer les détails vers la qualité voulue. Avec le temps, on apprend même à se tromper moins souvent du premier coup — non par dogme, mais à force de transformer du mauvais code en bon code.
Cela rejoint une idée que Burchard partage avec Kent Beck : « d'abord rendre le changement facile, puis faire le changement facile ». Le refactoring est l'étape qui rend le terrain praticable avant d'y bâtir la fonctionnalité.
// ❌ Avant : difficile d'ajouter une remise « fidélité » ici
function total(panier) {
let t = 0;
for (const a of panier.articles) t += a.prix * a.qte;
if (panier.client.premium) t = t * 0.9; // remise mêlée au calcul
return t;
}
// ✅ Après refactoring : on n'a RIEN changé au comportement,
// mais on a rendu le futur ajout de remise trivial
function total(panier) {
return appliquerRemises(sousTotal(panier), panier.client);
}
function sousTotal(panier) {
return panier.articles.reduce((t, a) => t + a.prix * a.qte, 0);
}
function appliquerRemises(montant, client) {
return client.premium ? montant * 0.9 : montant;
} Les mêmes entrées produisent les mêmes sorties : aucun test ne casse. Mais la prochaine remise (fidélité, code promo) s'ajoutera désormais dans une seule fonction dédiée, sans toucher au calcul du sous-total.
Le refactoring comme exploration
Améliorer le code n'est pas le seul usage du refactoring. Il sert aussi à comprendre ce sur quoi on travaille et à bâtir sa confiance de codeur. Transformer le code est un moyen de l'explorer : renommer, extraire, déplacer pour voir ce qui dépend de quoi.
Burchard pousse l'idée jusqu'à la liberté totale. Vous êtes plus important que votre code. Cassez-le, supprimez-le, changez tout ce que vous voulez : vous avez le contrôle de version. Ce qui se passe entre vous et votre éditeur ne regarde personne, et vous travaillez dans le médium le plus souple et le plus durable qui soit. On apprend beaucoup en écrivant des tests et en avançant à petits pas — mais ce n'est pas toujours la voie la plus rapide ni la plus jubilatoire de l'exploration. Quand l'envie est juste de démonter un morceau pour voir comment il marche, foncez : Git vous ramènera à la version saine.
La discipline des petits pas
Comment, concrètement, refactore-t-on en sécurité ? Par petits pas sûrs et réversibles, sous le cycle rouge-vert-refactor :
1. ROUGE / VERT : la suite de tests passe (le filet est en place).
2. REFACTOR : une petite transformation (un renommage, une extraction).
3. VERT ? : on relance les tests.
- tous au vert -> on garde, on commit, étape suivante.
- un au rouge -> on annule (git) et on recommence
par un pas encore plus petit. Chaque pas est assez réduit pour qu'en cas d'échec, le retour arrière soit immédiat. C'est ce qui distingue la méthode d'un grand chantier risqué. Voici les deux refactorings les plus élémentaires — qui, par construction, ne changent pas le comportement.
Renommer une variable obscure pour révéler l'intention :
// ❌ Avant : que représentent d, x, l ?
function f(d) {
let x = 0;
for (const l of d) x += l;
return x / d.length;
}
// ✅ Après : mêmes entrées, mêmes sorties, intention claire
function moyenne(notes) {
let somme = 0;
for (const note of notes) somme += note;
return somme / notes.length;
} Extraire une fonction au nom parlant pour nommer un calcul :
// ❌ Avant : un calcul anonyme noyé dans la condition
function peutCommander(user) {
if (user.age >= 18 && user.compteVerifie && !user.banni) {
return true;
}
return false;
}
// ✅ Après : le « pourquoi » est extrait dans un nom
function peutCommander(user) {
return estClientEligible(user);
}
function estClientEligible(user) {
return user.age >= 18 && user.compteVerifie && !user.banni;
} Dans les deux cas, lancez la suite de tests avant et après : elle doit rester verte de bout en bout. Si elle vire au rouge, vous n'avez pas refactoré — vous avez changé le comportement, et il faut annuler.
Réécriture n'est pas refactoring
Burchard termine sur une mise en garde de fond. La tentation, quand une base de code se dégrade, est de la réécrire (rewrite) dans le « framework du mois ». Reconstruire une application dans un nouveau framework figure justement dans sa liste de ce qui n'est pas du refactoring : c'est créer du code neuf, une démarche coûteuse et risquée.
Or les frameworks ne nous sauvent pas de nos problèmes de qualité. jQuery ne l'a pas fait ; ESNext, Ramda, Sanctuary, Immutable.js, React, Elm ou leurs successeurs ne le feront pas davantage. Réduire et organiser le code est utile, mais le vrai remède au cycle infernal — souffrir d'un code médiocre, le réécrire à grands frais, souffrir à nouveau, réécrire encore — est un processus d'amélioration progressive : le refactoring sous protection des tests. C'est ce que le reste du livre s'emploie à outiller.
À retenir
- Refactoring = améliorer la structure sans changer le comportement observable. Tout ce qui modifie le comportement (corriger un bug, ajouter une feature, optimiser, changer de framework) est « changer le code », pas du refactoring.
- Le test décisif : un changement d'interface doit casser des tests ; un changement de détail d'implémentation ne doit en casser aucun. Seules comptent les correspondances entrées/sorties.
- Sans filet de tests, ce n'est pas du refactoring, c'est du bricolage risqué. Du code non testé (legacy ou neuf) ne se refactore pas : on le couvre d'abord (tests de caractérisation pour le legacy).
- Tests + contrôle de version forment le filet : les tests prouvent que le comportement tient, Git permet d'annuler instantanément un pas raté.
- Avancez par petits pas sûrs et réversibles (rouge-vert-refactor) : renommer, extraire, déplacer, en gardant les tests au vert à chaque étape.
- Le but : qualité et préparation du futur — « d'abord rendre le changement facile, puis faire le changement facile ». La réécriture dans un nouveau framework n'est pas un raccourci ; les frameworks ne réparent pas la qualité.