Legacy code = code sans tests : la définition qui dérange, et les techniques pour s'en sortir sans tout réécrire.
Pourquoi ce livre
À une conférence Extreme Programming, Michael Feathers croise un collègue qui revenait de visiter une équipe. Il lui demande comment ça se passe. Le collègue répond : "Ils écrivent du code legacy, mec." Feathers dit avoir pris ce commentaire comme un coup de poing dans l'estomac.
Pas parce que l'équipe était incompétente ou paresseuse, mais parce qu'ils travaillaient sans filet : chaque changement était une pari, chaque modification pouvait casser quelque chose sans qu'on le sache.
Feathers a passé des années à aider des équipes coincées dans de grandes bases de code enchevêtrées. Il réglait les mêmes problèmes encore et encore : comment instancier une classe dans un cadre de tests quand son constructeur appelle une base de données, comment ajouter une fonctionnalité sans en casser trois autres. Il a consigné les techniques pour que d'autres équipes n'aient pas à les redécouvrir seules.
Les idées qui restent
1Legacy code, ce n'est pas du vieux code. C'est du code sans tests.
La majorité des développeurs utilisent "code legacy" comme formule polie pour "le bazar de quelqu'un d'autre". Feathers propose une autre définition : "pour moi, legacy code, c'est simplement du code sans tests." (Préface). L'implication est gênante.
Du code qu'on a écrit soi-même la semaine dernière, sans tests, est déjà du legacy. Un beau monolithe soigneusement renommé et refactoré, mais sans couverture de tests, est du legacy.
Pourquoi ce critère ? "Du code sans tests, c'est du mauvais code. Peu importe qu'il soit bien écrit, joli, orienté objet ou bien encapsulé. Avec des tests, on peut modifier le comportement rapidement et de façon vérifiable. Sans eux, on ne sait pas vraiment si le code s'améliore ou se dégrade." (Préface). Cette phrase est la thèse du livre. Tout le reste est technique.
2Le paradoxe de la poule et de l'œuf (et comment en sortir)
La logique cruelle du code legacy : pour le modifier en sécurité, il faut des tests ; pour ajouter des tests, il faut souvent modifier le code. Feathers appelle ça "The Legacy Code Dilemma" et refuse de prétendre que ça n'existe pas. Il propose plutôt un algorithme en cinq étapes comme rythme quotidien du travail en legacy : (1) identifier les points de changement, (2) trouver les points de test, (3) casser les dépendances, (4) écrire les tests, (5) faire les changements et refactorer (ch. 2).
L'objectif quotidien n'est pas "zéro bug". C'est d'étendre le territoire couvert par les tests, un peu plus à chaque changement, jusqu'à ce que les îlots de code maîtrisé deviennent des continents. Une conquête de terrain incrémentale, pas une révolution.
3Les séams : modifier le comportement sans toucher à la classe
Feathers introduit son concept le plus original. L'idée concrète d'abord : quand une classe instancie directement son collaborateur avec new MonService(), il n'y a pas de couture. Pour que les tests utilisent un autre service, il faut ouvrir la classe et la modifier. Quand elle reçoit le service par injection au constructeur, il y a une couture : on passe ce qu'on veut (le vrai service en production, un faux en test) sans jamais toucher au code de la classe.
Le comportement change de l'extérieur. C'est pour ça que Feathers définit un séam comme "un endroit où on peut modifier le comportement du programme sans éditer à cet endroit" (ch. 4) : la classe ne bouge pas, c'est ce qu'on lui donne qui change.
Chaque couture a un "enabling point" : l'endroit précis où on décide quel comportement utiliser. Feathers distingue trois types selon le langage : macros C, paramètres du linker Java et couture objet. Pour un développeur PHP ou JavaScript, un seul compte : la couture objet. C'est le constructeur. On passe la dépendance au moment de créer l'objet, et on peut passer ce qu'on veut sans modifier la classe.
Le modèle des séams n'est pas une histoire de mocks ou de frameworks. C'est une façon de lire son code pour voir ses articulations.
new MonService() est gravé dans la classe : impossible de le remplacer dans les tests. L'interface injectée crée la couture. Le constructeur est l'enabling point.4Sensing and Separation : voir et isoler
Quand une classe résiste aux tests, la cause est presque toujours l'une de deux choses. Le sensing : la classe calcule des valeurs qu'on ne peut pas observer de l'extérieur (elles partent directement dans une base de données ou un écran). La separation : on ne peut pas instancier la classe dans un test sans embarquer la moitié du système avec elle.
La solution dans les deux cas : les "fake objects", des objets imposteurs qui jouent le rôle des vrais collaborateurs pendant les tests. L'exemple du livre : une classe Sale qui écrit sur un écran d'encaissement. On extrait une interface Display avec deux implémentations : ArtR56Display en production et FakeDisplay dans les tests. Sale ne change pas d'un octet. Le FakeDisplay enregistre ce qui a été affiché pour qu'un test puisse le vérifier.
C'est exactement ce que Mockery dans PHPUnit ou jest.mock() font. Feathers explique le mécanisme derrière l'outil.
5Sprout and Wrap : ajouter du code testé sans toucher au code non testé
Le scénario classique : vous devez ajouter une fonctionnalité dans une méthode de 50 lignes non testée. Modifier ces 50 lignes risque de tout casser. Ne rien toucher, c'est ne jamais progresser. Feathers propose deux sorties de secours.
Sprout Method (méthode pousse) : écrivez la nouvelle logique dans une méthode à part, testable seule, puis appelez-la depuis l'ancienne avec une seule ligne ajoutée. L'ancien code gagne une ligne, rien d'autre ne change.
function pay($amount) {
// ...50 lignes de code legacy inchangées...
$this->sendConfirmation($amount); // ← la seule ligne ajoutée
}
private function sendConfirmation($amount) { // ← nouvelle, testable seule
// votre nouvelle logique ici
}
Wrap Method (méthode enveloppe) : renommez l'ancienne méthode (pay() devient legacyPay()), puis créez une nouvelle pay() qui appelle votre code neuf et délègue ensuite à legacyPay(). Tous les appelants continuent d'appeler pay() : rien ne casse, et votre nouveau code est testable séparément.
function pay($amount) { // ← même nom public, rien ne casse
$this->logPayment($amount); // nouvelle logique, testable seule
$this->legacyPay($amount); // ancien code, juste rebaptisé
}
private function legacyPay($amount) { /* 50 lignes inchangées */ }
Ces techniques ne nettoient pas l'existant. Elles créent des coutures dans du code qui n'en avait pas, pour que les prochaines retouches puissent s'appuyer sur des tests.
6Les tests de caractérisation : documenter ce que le code fait vraiment
Le chapitre le plus contre-intuitif. Dans du code legacy sans tests, on ne sait généralement pas ce que le code est censé faire. Seulement ce qu'il fait. La réponse de Feathers : écrire des tests qui capturent le comportement réel, pas le comportement voulu.
Un test de caractérisation se construit en quatre gestes. On écrit une assertion qu'on sait fausse : par exemple assertEquals("fred", generator.generate()). On lance le test et on lit le message d'erreur : expected: <fred> but was: <>. On recopie cette vraie valeur comme valeur attendue. Le test passe.
"Ces tests n'ont aucune autorité morale ; ils sont là, assis, documentant ce que les morceaux du système font vraiment." (ch. 13). Cette formulation est libératrice.
On ne cherche pas de bugs. On installe un déclencheur d'alarme : si quelqu'un, y compris soi-même dans six mois, change ce comportement accidentellement, le test le captera. Les tests de caractérisation sont le filet de sécurité qu'on tend avant de commencer le vrai refactoring.
7Scratch Refactoring : comprendre le code en le jetant
Ouvrir une branche temporaire. Refactorer agressivement : extraire des méthodes, renommer des variables, déplacer des blocs, dessiner la structure. Comprendre comment ça s'articule vraiment. Puis jeter ce code. Ne pas commiter. Git (ou tout autre outil de versionnement) garde l'original : on peut toujours revenir. Le refactoring n'était pas le but, la compréhension l'était.
Feathers appelle ça le Scratch Refactoring. La première fois qu'il l'a décrit à un collègue, le collègue y a vu du gâchis. Après une demi-heure à bouger des choses, le collègue était convaincu.
La technique se combine avec ce que Feathers appelle le Listing Markup (annotation sur papier) : on imprime le code et on le parcourt au marqueur couleur. Une couleur par groupe de lignes qui travaillent ensemble. Des traits qui délimitent les blocs imbriqués depuis l'intérieur vers l'extérieur, pour révéler la structure en pelant les couches. Des cercles autour des sections qui pourraient être extraites.
On a l'air de ne rien faire. On construit en réalité une carte mentale du code qu'aucun IDE n'affiche.
8Hyperaware Editing : une chose à la fois
Chaque frappe clavier change le comportement du logiciel, ou ne le change pas. Formater un commentaire : aucun changement. Modifier un littéral de chaîne dans du code actif : un changement. Feathers appelle la conscience de cette frontière l'"hyperaware editing" : savoir à chaque instant si on refactore ou si on ajoute du comportement.
La condition pour rester dans cet état : une suite de tests rapide. Quand les tests tournent en moins d'une seconde, chaque geste reçoit une réponse avant le suivant.
"La programmation est l'art de faire une chose à la fois." (ch. 23). De là vient le Single-Goal Editing (n'éditer qu'avec un seul objectif en tête) : quand une idée parasite surgit (cette autre méthode mériterait d'être nettoyée), on la note sur papier. On revient à ce qu'on faisait. On le finit. On lance les tests. Seulement ensuite on ouvre l'item suivant.
Les équipes qui sautent cette discipline font du "thrashing" : cinq demi-chantiers emmêlés, un build cassé, deux heures de débogage pour démêler tout ça. La note sur papier n'est pas de la procrastination ; c'est de la précision.
Mon avis, honnêtement
La définition seule vaut le déplacement. Si on retient une seule chose de ce livre ("legacy code, c'est du code sans tests"), le livre a rempli son contrat. Le modèle des séams est le deuxième concept le plus précieux : il apprend à lire le code différemment, en cherchant des articulations plutôt que de la logique. Et les tests de caractérisation résolvent un problème que personne ne formule assez clairement : comment commencer à tester quand on ne sait pas ce que "correct" veut dire pour ce code.
Les défauts honnêtes : le livre date de 2004 et chaque exemple est en Java, C++ ou C. Dans la pratique, le système de mocks de PHPUnit ou jest.mock() rend inutile une grande partie du câblage manuel décrit ici.
La Part II (vingt chapitres en format FAQ) devient répétitive : la technique est souvent les mêmes quatre gestes habillés dans un nom de classe différent. On ne le lit pas de la première à la dernière page ; on l'ouvre sur le chapitre qui correspond au problème du moment.
Ce que je trouve vraiment honnête dans ce livre : Feathers reconnaît explicitement que ses techniques produisent souvent du code plus laid à court terme. Casser une dépendance pour mettre des tests en place peut nécessiter d'ajouter un paramètre au constructeur qui ne sert à rien en production, ou de rendre public une méthode privée.
Il dit que la cicatrice est temporaire : une fois les tests en place, le vrai refactoring peut commencer. Cette franchise rend le livre fiable.
Odilon
Toujours valable en 2026 ?
L'IA fait exactement ce que Feathers décrit : elle écrit des tests de caractérisation qui photographient le comportement actuel, elle refactore pendant que les tests surveillent. Donnez-lui le protocole du livre, elle l'exécute bien. Les snapshots de Jest, c'est le même concept rendu sans friction par l'outillage. Sur ce point, le livre a été rattrapé.
Le problème, c'est avant. Si le constructeur instancie directement un client Stripe, si la config globale doit être initialisée trois couches au-dessus, si la classe fait six choses à la fois : l'IA ne peut pas tester ce code, pour la même raison que vous ne le pouviez pas. Il faut d'abord créer les séams. Ce travail-là, le livre l'enseigne.
L'IA ne peut pas le décider à votre place : elle ne sait pas si telle dépendance peut être injectée sans casser la production.
La syntaxe des exemples vieillit. Le problème qu'ils illustrent, non.
Pour qui ?
Lisez-le si
- Vous maintenez une base de code avec peu ou pas de tests et chaque modification ressemble à un désamorçage de bombe
- Vous avez essayé d'ajouter des tests à une classe existante et vous vous êtes heurté à un mur de dépendances
- Vous pilotez une équipe qui travaille en legacy et cherchez un vocabulaire commun pour nommer les problèmes
- Vous avez lu Clean Code mais restez bloqué sur la question "par où commencer"
Passez votre chemin si
- Vous partez de zéro avec des tests dès le premier commit : ce livre est un manuel de survie pour une situation dans laquelle vous n'êtes pas. Ressortez-le le jour où quelqu'un vous passera une base de code sans tests
- Vous connaissez déjà Sprout/Wrap et les tests de caractérisation : le reste est de la documentation de référence, pas une lecture
- Les exemples Java/C++ vous rebutent : les concepts sont solides, mais lire autour de la syntaxe demande un effort
Pour aller plus loin
Les techniques de tests de ce livre se pratiquent dans mon cours sur les tests. Côté bibliothèque, Refactoring (Fowler) est le prolongement naturel une fois les tests en place : Feathers vous amène à la ligne de départ, Fowler montre la course. Clean Code (Martin) couvre les principes de design qui guident ce que "mieux" signifie après l'installation des tests. En pratique, l'article sur l'audit de code Symfony legacy montre comment appliquer ces principes sur un vrai projet de reprise.
Commentaires (0)