Refactorer les structures simples
Variables, conditions et boucles : les transformations de base qui rendent le code lisible, une étape sûre à la fois.
Avant de s'attaquer aux fonctions et aux objets, Burchard commence par les briques élémentaires du code : les variables, les conditions et les boucles. Ce sont les « accords » du programme, pour reprendre sa métaphore musicale : des assemblages minuscules dont la difficulté détermine celle de l'ensemble. Les refactorings de ce chapitre sont de bas niveau (low-level) : ils portent sur de petits morceaux de code, le genre de choses qu'un linter pourrait signaler ou qu'un éditeur pourrait « refactorer automatiquement ».
Le fil rouge du livre est un classifieur bayésien naïf (Naive Bayes Classifier, ou NBC) qui devine la difficulté d'une chanson à partir de ses accords. Le code de départ est « un peu plus de 100 lignes de charabia assez incompréhensible ». Plutôt que de tout comprendre d'abord, Burchard propose la démarche inverse : développer la confiance par le test, puis refactorer par petits pas. Chaque transformation suit le même cycle — save / run / check / commit — sous la protection d'un test, même rudimentaire.
À retenir
Avant tout refactoring, il faut un filet. Ici le « test » est manuel : on lance node nb.js, on note la sortie, et on vérifie après chaque changement qu'elle est identique. Dans le pire des cas, on fait git checkout . pour revenir à la dernière version saine. Sans cette boucle de vérification, la plupart des transformations qui suivent seraient imprudentes.
Renommer : le refactoring le plus simple
Le refactoring le plus facile consiste à renommer ce qui n'a pas de sens : variables, fonctions, objets ou modules. C'est un excellent point de départ, et dans le pire des cas il casse le programme, ce que le test révèle aussitôt.
Burchard part en chasse des mauvais noms : mots mal orthographiés, noms d'une seule lettre, noms vagues, chiffres dans les identifiants, conventions non respectées (snake_case au lieu de camelCase). Dans le NBC, plusieurs cibles sautent aux yeux.
// ❌ Avant : noms cryptiques, conventions incohérentes.
somewhere_over_the_rainbow = ['c', 'em', 'f', 'g', 'am'];
var song_11 = [];
function classify(chords){
var ttal = labelProbabilities; // mot tronqué
Object.keys(ttal).forEach(function(obj){ // « obj » : trop vague
// ...
});
} // ✅ Après : des noms réels, parlants, conformes au camelCase.
var somewhereOverTheRainbow = ['c', 'em', 'f', 'g', 'am'];
var blankSong = []; // plus spécifique que « songEleven »
function classify(chords){
var total = labelProbabilities;
Object.keys(total).forEach(function(difficulty){
// ...
});
} Le cas des indices i et j
Les variables d'une seule lettre méritent une mention spéciale. Dans une boucle for, lorsqu'une variable représente une vraie clé numérique, index (et innerIndex si besoin) est préférable à i. Burchard rappelle que ces noms courts sont résistants au changement : un chercher/remplacer de i risque de toucher chaque i niché dans un mot plus long, transformant une opération anodine en chasse manuelle pénible.
Quand i représente non un indice mais une clé de chaîne d'un objet ("easy", "medium", "hard"), il faut s'appuyer sur le domaine métier. Un console.log(i) rapide dans la boucle révèle la valeur et suggère un nom : ici, difficulty ou chord selon le cas.
Astuce
Renommer peut avoir un large impact. Dans un programme d'un seul fichier, assurez-vous de changer toutes les occurrences. Dès que le code s'étale sur plusieurs fichiers, appuyez-vous sur votre éditeur ou la ligne de commande (grep, ack) pour les trouver toutes. Et pour un module public, prévoyez des avertissements de dépréciation.
Supprimer le code inutile
Le principe est limpide : si vous n'en avez pas besoin, jetez-le (c'est le principe YAGNI, « Ya Ain't Gonna Need It »). Le code inutile prend plusieurs formes : code mort (variables ou fonctions jamais utilisées), code spéculatif et commentaires d'intention, espaces superflus, code qui ne fait rien, et instructions de débogage oubliées.
Le code mort (dead code) se repère facilement : cherchez une seule occurrence d'un nom dans le projet. Dans le NBC, les variables army et blankSong ne servent nulle part : leurs déclarations partent à la corbeille. La ligne fs = require('fs') est du code spéculatif (speculative code) — une intention jamais réalisée d'utiliser le système de fichiers.
Attention
Ne vous contentez pas de commenter le code mort. Un code commenté laisse croire qu'il fonctionnerait si on le décommentait — alors que rien ne le garantit. Les intentions futures appartiennent à un gestionnaire de tâches (tickets, user stories), pas au code source. Et il faut toujours faire confiance au code réellement exécuté, jamais à du code endormi.
Le code qui ne fait rien
Le code qui ne fait rien (do-nothing code) est exécutable mais sans effet. Burchard en montre plusieurs cas. D'abord la double négation inutile.
// ❌ Avant : le !! convertit en booléen une valeur déjà booléenne.
if(!!(Object.keys(labelCounts).includes(label))){
// ✅ Après : includes renvoie déjà un booléen.
if(Object.keys(labelCounts).includes(label)){ Dans un if, JavaScript suit la branche true pour toute valeur « vraie » (truthy) sans coercition explicite. Le !! n'a sa place que devant la valeur de retour d'une fonction qu'on veut forcer en booléen.
Autre exemple : une conversion * 1.0 héritée de langages où 10 / 3 vaut 3. En JavaScript, pas d'entiers ni de flottants distincts, juste des number : la division produit déjà 3.3333..., et le * 1.0 est superflu.
// ❌ Avant
probabilityOfChordsInLabels[difficulty][chord] =
probabilityOfChordsInLabels[difficulty][chord] * 1.0 / songs.length;
// ✅ Après
probabilityOfChordsInLabels[difficulty][chord] =
probabilityOfChordsInLabels[difficulty][chord] / songs.length; Une variable temporaire (temp variable) peut elle aussi ne rien apporter. La variable total ne fait que recevoir une affectation puis être journalisée : on peut logger labelProbabilities directement.
// ❌ Avant : une variable temporaire qui n'apporte rien.
function classify(chords){
var total = labelProbabilities;
console.log(total);
var classified = {};
Object.keys(total).forEach(function(difficulty){
// ✅ Après : on utilise la valeur directement.
function classify(chords){
console.log(labelProbabilities);
var classified = {};
Object.keys(labelProbabilities).forEach(function(difficulty){ Simplifier les conditionnelles
Les conditions sont le terrain le plus rentable de ce chapitre. Une conditionnelle peut être inutilement complexe, négative ou redondante. Reprenons un bloc du classifieur.
// ❌ Avant : la branche « true » ne fait rien d'utile.
if(probabilityOfChordInLabel === undefined){
first + 1.01; // n'affecte rien, n'a aucun effet de bord
} else {
first = first * (probabilityOfChordInLabel + 1.01);
} La branche true calcule first + 1.01 sans rien en faire : aucun effet, aucune affectation. Seule la branche else compte. La première transformation inverse la condition pour ne garder que le cas utile.
// Étape 1 : on aplatit en gardant uniquement le cas « else ».
if(probabilityOfChordInLabel !== undefined){
first = first * (probabilityOfChordInLabel + 1.01);
} Éviter les conditions négatives et trop spécifiques
Le test !== undefined est trop spécifique : tout ce qui importe vraiment, c'est que probabilityOfChordInLabel soit une valeur « vraie ». Une condition positive et simple se lit mieux qu'une condition négative chargée de détails.
// ✅ Après : condition positive, juste ce qui compte.
if(probabilityOfChordInLabel){
first = first * (probabilityOfChordInLabel + 1.01);
} Piège courant
Passer de !== undefined à une simple condition « truthy » change la sémantique pour les valeurs « fausses » (0, "", NaN…). Burchard est explicite : « sans procédure de test en place, ce changement serait une mauvaise idée. » C'est précisément parce que la sortie reste identique au lancement qu'on peut se le permettre. Le filet de test rend l'audace acceptable.
La duplication dans les conditionnelles
Une autre forme de code inutile : répéter une instruction dans les deux branches. Si la même action survient quoi qu'il arrive, sortez-la de la conditionnelle.
// ❌ Avant : on nourrit le chien dans les deux branches.
if(dog.weight > 40){
buyFood('big bag');
dog.feed();
} else {
buyFood('small bag');
dog.feed();
}
// ✅ Après : l'action commune est factorisée.
if(dog.weight > 40){
buyFood('big bag');
} else {
buyFood('small bag');
}
dog.feed(); Variables : nombres magiques, fonctions intégrées, portée
Remplacer un nombre magique par une constante
Un nombre magique (magic number) est une valeur codée en dur qui « surgit de nulle part ». Tous les nombres ne sont pas magiques : dans le NBC, les 0 et 1 servent d'indices ou de compteurs et ne méritent pas de nom. Mais le 1.01 qui apparaît plusieurs fois dans classify est, lui, suffisamment énigmatique. Règle : nommer la valeur et la déclarer dans la portée la plus petite possible. Comme 1.01 est confiné à classify, la constante vit en haut de cette fonction.
// ❌ Avant : 1.01 répété, sans explication.
var first = labelProbabilities[difficulty] + 1.01;
// ...
first = first * (probabilityOfChordInLabel + 1.01);
// ✅ Après : un nom révèle l'intention (lissage de l'algorithme).
function classify(chords){
var smoothing = 1.01;
// ...
var first = labelProbabilities[difficulty] + smoothing;
// ...
first = first * (probabilityOfChordInLabel + smoothing);
} Au-delà du manque d'explication, le nombre magique résiste au changement : aucun endroit unique où ajuster sa valeur. Burchard prend l'image d'un jeu où la gravité 9.8 serait éparpillée partout : faire un niveau sur la Lune deviendrait une chasse au trésor au lieu d'un changement en un point.
Les chaînes magiques (magic strings) suivent la même logique. Les noms d'accords sont trop variés pour qu'en faire des variables apporte quoi que ce soit. En revanche, les niveaux de difficulté sont répétés et susceptibles d'évoluer ('medium' → 'intermediate'), donc on les promeut en variables.
// ✅ Déclaré une fois, en tête de fichier.
var easy = 'easy';
var medium = 'medium';
var hard = 'hard';
// puis on remplace 'easy' par easy, etc., dans tout le programme. Note
Ces variables sont ici globales, ce qui n'est pas idéal. Mais Burchard le concède : tant que c'est confiné à un seul fichier, une globale reste « toujours meilleure que la même chaîne magique répétée partout ». Le mieux est l'ennemi du bien quand on procède par petits pas.
Intégrer un appel de fonction et supprimer la variable temporaire
Quand le résultat d'une fonction ne sert qu'à alimenter une variable utilisée une seule fois, on peut intégrer l'appel (inline function) puis supprimer la variable.
// ❌ Avant : neuf lignes, une fonction et une variable de trop.
function getNumberOfSongs(){
return songs.length;
}
function setLabelProbabilities(){
Object.keys(labelCounts).forEach(function(label){
var numberOfSongs = getNumberOfSongs();
labelProbabilities[label] = labelCounts[label] / numberOfSongs;
});
} On remplace l'appel par le corps de la fonction, puis comme getNumberOfSongs n'est plus appelée nulle part, elle devient du code mort que l'on supprime. Enfin, numberOfSongs n'étant utilisée qu'une fois, on l'élimine aussi.
// ✅ Après : cinq lignes au lieu de neuf.
function setLabelProbabilities(){
Object.keys(labelCounts).forEach(function(label){
labelProbabilities[label] = labelCounts[label] / songs.length;
});
} La condition de sécurité : les variables locales et le this éventuel de la fonction intégrée doivent rester accessibles. Ici, getNumberOfSongs ne dépendait que de la variable partagée songs, donc aucun ajustement n'est nécessaire.
Introduire une variable explicative
L'inverse est parfois utile : introduire une variable (extract variable) pour nommer une sous-expression ou raccourcir une ligne trop longue. Mais Burchard prévient : les variables sont moins souples que les fonctions, car les extraire augmente leur portée et leurs responsabilités, ce qui rapproche des globales. Quand on peut, mieux vaut extraire une fonction. L'usage le plus net reste de mettre en cache un calcul coûteux répété.
// ❌ Avant : la sélection DOM (coûteuse) est refaite trois fois.
$('#someDomElement').css('width', 5);
$('#someDomElement').css('background-color', 'red');
$('#someDomElement').show();
// ✅ Après : une variable de cache, la requête n'est exécutée qu'une fois.
var domElement = $('#someDomElement');
domElement.css('width', 5);
domElement.css('background-color', 'red');
domElement.show(); Lorsque l'interface le permet, le chaînage d'appels (method chaining) est encore meilleur que la variable de cache : chaque méthode renvoie this, ce qui rend la variable intermédiaire inutile.
// ✅ Encore mieux : on enchaîne, plus aucune variable.
$('#someDomElement')
.css('width', 5)
.css('background-color', 'red')
.show(); Réduire la portée et comprendre le hoisting
JavaScript possède une particularité déroutante : le hoisting. Une variable déclarée avec var est initialisée à undefined en haut de sa portée de fonction ; une déclaration de fonction (function f(){}), elle, est remontée entièrement, valeur comprise. Si cela risque d'embrouiller l'équipe, on peut déclarer la variable explicitement en tête, là où elle est de toute façon remontée.
// La déclaration descendue dans le for est en réalité « hoistée ».
function train(chords, label){
var index; // équivaut à : var index = undefined;
songs.push([label, chords]);
labels.push(label);
for (index = 0; index < chords.length; index++){
// ...
}
} Astuce
Le hoisting distingue deux formes de déclaration. function f(){} est intégralement remontée : on peut appeler f() avant sa déclaration. À l'inverse, var f = function(){} ne remonte que le nom f, initialisé à undefined — appeler f() avant lèvera TypeError: f is not a function. Le chapitre suivant approfondira var, let et const pour mieux maîtriser la portée.
Boucles : choisir la bonne, ou s'en passer
Les boucles « impressionnent » au début, mais ce qu'on veut vraiment, c'est manipuler des ensembles de valeurs et leur appliquer des fonctions. Burchard décline la même boucle du NBC sous plusieurs formes pour montrer l'évolution vers une expression plus claire.
// ❌ Avant : un for impératif, avec gestion manuelle de l'index.
for (index = 0; index < chords.length; index++){
if(!allChords.includes(chords[index])){
allChords.push(chords[index]);
}
} Les variantes while et do...while déplacent simplement les trois aspects (initialisation ; condition ; mise à jour) sans rien gagner. La vraie question est : a-t-on besoin de l'index ? Si non, for...of libère complètement de cette mécanique.
// Mieux : for...of, « pour chaque accord parmi les accords ».
for (let chord of chords){
if(!allChords.includes(chord)){
allChords.push(chord);
}
} Attention
Évitez for...in pour parcourir un tableau. L'ordre des indices n'est pas garanti, toute propriété énumérable (y compris héritée du prototype) sera parcourue, et modifier le tableau pendant la boucle sème la confusion. for...of n'a aucun de ces pièges quand on ne se soucie pas des indices numériques.
De la boucle à forEach et map
L'étape suivante remplace for...of par forEach, que Burchard décrit comme « la porte d'entrée vers la programmation fonctionnelle ». Même si le gain de lignes est minime, l'approche est plus souple et permet d'extraire le corps de boucle en fonction nommée et réutilisable.
// ✅ forEach : le corps de boucle peut devenir une fonction nommée.
function checkAndInclude(chord){
if(!allChords.includes(chord)){
allChords.push(chord);
}
}
chords.forEach(checkAndInclude); forEach reste un effet de bord : on pousse dans un tableau existant. Lorsqu'on transforme un tableau en un autre, map exprime mieux l'intention — « appliquer une fonction au tableau » plutôt que « initialiser un tableau puis y empiler des éléments ».
// ❌ Avant : forEach + push, impératif et verbeux.
var newArray = [];
[2, 3, 4].forEach(element => {
newArray.push(element * 2);
});
// ✅ Après : map décrit la transformation, sans tableau intermédiaire.
var newArray = [2, 3, 4].map(element => element * 2); Note
Ne vous laissez pas happer par les débats de performance entre constructions de boucles. Le conseil de Burchard : mesurez (benchmark) et corrigez les parties réellement lentes. Ce ne sont presque jamais vos boucles JS qui ralentissent votre code ; et si c'est le cas, alors corrigez-les — mais seulement alors.
À retenir
- Toujours sous filet de test, par petits pas :
save / run / check / commit, etgit checkout .si ça casse. C'est ce qui rend les transformations audacieuses acceptables. - Renommer et supprimer d'abord : remplacez les noms cryptiques (
ttal,obj,i/j) par des noms parlants, et éliminez le code mort, spéculatif ou qui ne fait rien (!!,* 1.0, variables temporaires inutiles). - Simplifier les conditions : inversez pour garder le seul cas utile, préférez une condition positive et « juste assez large » à une condition négative trop spécifique, et factorisez les actions communes hors des branches.
- Nommer les valeurs magiques : remplacez nombres et chaînes codés en dur par des constantes, dans la portée la plus petite possible.
- Variables : avec parcimonie : intégrez les appels de fonction et supprimez les variables temporaires à usage unique ; introduisez une variable surtout pour mettre en cache, mais préférez l'extraction de fonction ou le chaînage.
- Boucles : monter en abstraction : passez du
forindexé àfor...of, puis àforEach(corps extractible) et enfin àmapquand vous transformez un tableau — l'intention prime sur la mécanique.