J'ai participé à la Master Class de Michael Feathers sur le code Legacy : voici ce que vous pouvez en tirer vous aussi !
Temps de lecture : 5min
J’ai eu l’opportunité de participer à la Master Class : Reducing Technical Debt de Michael Feathers.
Pendant 3 jours, nous avons ainsi vu comment réduire la dette technique, notamment dans le contexte d’un code legacy.
Je vous propose donc ces prochaines semaines de vous partager ce que j’ai pu apprendre !
Vous découvrirez dans ce premier post :
Comment définir la dette technique en sortant de la métaphore économique ?
Comment savoir où commencer le refactoring du code ?
Comment appréhender le code legacy duquel on a hérité quand il semble totalement incompréhensible ?
Ready ? Let’s go !
De la vraie définition de la dette technique
La notion de dette technique est à l’origine une métaphore du milieu financier. On “achète” du temps emprunté sur le futur, pour aller temporairement plus rapidement. À terme, il faudra “rembourser” cette dette, c’est-à-dire prendre du temps pour stopper les évolutions dans le code et nettoyer le code actuel.
Mais qu’est-ce que ça veut dire “nettoyer” le code ? Pourquoi on vient à dire qu’un code est “sale” et qu’il fait donc partie de la dette technique ?
Pour pallier cette définition trop métaphorique, Michael Feathers propose une définition beaucoup plus concrète :
“La dette technique est l’ampleur de l’effort nécessaire pour refactorer le code pour rendre simple et non invasif, l’implémentation de la prochaine fonctionnalité”
Il convient de définir quelques notions dans cette description :
qu’appelle-t-on “refactorer” ?
que signifie le terme “non invasif” ?
Qu’est-ce que le refactoring finalement ?
Comme le décrit très bien Martin Fowler, le refactoring c’est l’action de modifier le code sans modifier le comportement sous-jacent.
D’un point de vue technique, cela se traduit par une série de petites étapes pour passer d’un état valide à un autre état valide du système, grâce aux tests.
Cela sous-entend que la condition sine qua none au refactoring est la présence de tests automatisés. J’y reviendrai :)
Qu’est-ce qu’un code non invasif ?
Un code non invasif est un code qui ne va pas “envahir” le code existant. Ok, ça sonne un peu “Captain Obvious”. Prenons un exemple :
Vous venez de louer un appartement dans un immeuble. Un nouveau locataire vient de louer l’appartement juste en dessous de vous. Il se trouve que l’immeuble est tellement mal conçu qu’à chaque fois que votre voisin du dessous doit faire couler de l’eau, il doit d’abord venir chez vous pour ouvrir l’eau. Vous vous sentiriez “envahi” n’est-ce pas ?
C’est la même chose pour le code : un code invasif, c’est un code qui va devoir modifier un code fonctionnel existant pour y ajouter un comportement supplémentaire.
Si cela vous rappelle quelque chose, c’est normal, c’est tout simplement la définition du Open-Closed Principle :)
Ces deux notions peuvent se résumer par la célèbre phrase de Kent Beck :
“First make the change easy, then make the easy change” - Kent Beck
Le workflow à appliquer est donc :
J’ai une nouvelle fonctionnalité à ajouter
Est-ce que je peux l’ajouter sans modifier le comportement du code existant ?
Oui : je l’implémente
Non : je passe à l’étape 3
L’implémentation est invasive, donc je rends le futur changement facile à implémenter en refactorant le code
Je peux maintenant implémenter la fonctionnalité de façon non invasive.
Simple non ?
Pas si vite…La citation complète de Kent Beck c’est :
“First make the change easy (warning: this might be hard), then make the easy change” - Kent Beck
Et cette parenthèse, c’est le cœur de l’aspect technique de notre métier. Surtout quand on hérite d’un code déjà bien bordélique…
Comment prendre en main du code legacy ?
Lorsque l’on est face à un code que l’on ne comprend pas, il ne sert à rien de se lancer dans des refactoring hasardeux. Michael Feathers propose une technique qui permet à une équipe de mieux comprendre le code qu’elle a sous les yeux : le scratch refactoring
Scratch Refactoring
Cette technique consiste à refactorer pas à pas, en faisant de tout petits refactoring, simples, et surtout temporaires.
J’insiste sur ce dernier point, le but ici n’est pas de réellement refactorer le code, mais de le débroussailler pour mieux le comprendre.
Exit donc les IDE, ici on va plutôt ouvrir notre bon vieux Notepad++. L’idée est juste de comprendre ce que fait le code en refactorant dans des méthodes temporaires pour rendre le code plus descriptif et compréhensible.
L’idée est donc de faire ces extractions de méthodes temporaires à mesure que vous lisez le code :
“ah je crois que ces quelques lignes sont responsables de calculer le score total du joueur, mettons tout ça dans une méthode computePlayerScore() !”.
Ce faisant, vous réduisez le gros pâté de code en plusieurs fonctions plus descriptives, ce qui vous permet d’avoir une meilleure vision d’ensemble du code.
Ces fonctions sont temporaires, elles ne sont là que pour vous permettre d’avoir ce nouveau niveau de lecture sur le code. En séparant en petites parties le code, les responsabilités et les frontières commencent à apparaître, et donc aussi les différentes couches ! On développe ainsi des pistes pour le vrai refactoring.
Mais attention ! Il est possible avec cette technique de faire de grosses erreurs de compréhension en refactorant et penser que le système fait quelque chose qu’il ne fait en fait pas du tout.
On peut aussi trop s’attacher au code jetable que l’on a écrit et donc commencer à le considérer comme du code qui va rester.
Après une session de scratch refactoring : git reset —hard
Pour se lancer dans le vrai refactoring, il nous faut des tests ! Et bien souvent le code legacy n’a pas de test, par définition.
Comment faire pour instaurer des tests dans un code que l’on comprend encore mal ?
On peut utiliser ici une technique de test particulière : le characterization testing
Characterization Testing
L’idée d’un characterization test est de générer le résultat d’un test en fonction du comportement actuellement observé.
Voici comment s’y prendre :
Isoler le morceau de code à tester pour pouvoir l’utiliser dans un test
Écrire une assertion qui va volontairement échouer (avec Jest on peut par exemple utiliser les snapshots avec l’assertion toMatchInlineSnapshot())
Laisser le message d’erreur nous guider sur le comportement attendu (avec Jest c’est ici que l’on va donc laisser le snapshot se générer)
Changer le test pour que l’assertion corresponde au résultat attendu (avec Jest il n’y a rien à faire, cette étape est fusionnée avec la 3)
Recommencer
Cette méthode permet de “demander” au code ce qu’il fait. Il devient la source de vérité.
Heuristique pour écrire des Characterization Tests
Écrivez des tests pour la surface de code que vous êtes amenés à toucher quand vous souhaitez modifier / ajouter une fonctionnalité à du code legacy. Écrivez le maximum de scénarios possibles pour bien comprendre le comportement du code.
Après avoir fait ça, réfléchissez aux éléments spécifiques que vous vous apprêtez à changer et écrivez des tests pour ça.
Si vous êtes tentés d’extraire ou de déplacer des fonctionnalités, écrivez des tests qui vérifient l’existence et le fonctionnement de ces comportements à un niveau d’abstraction supérieur, comme un use case par exemple. Une fois que les tests passent, alors vous pouvez faire le changement.
Ces techniques mettent fin au premier article de cette série. J’ai encore énormément de choses à vous dire sur le sujet, car la Master Class était riche en enseignements !
En attendant : Happy Coding :)