Legacy Refactoring : un fléau ? Non, un challenge !

Aujourd’hui, une nouvel article pour sur les pratiques du software craftsmanship.
Au menu, le Legacy Refactoring, deux mots complexes pour signifier quelque chose de… bien complexe dans la vie d’un développeur.

  • Le mot refactoring parle normalement à tout le monde. Il s’agit de retravailler le code pour en améliorer la lisibilité, la compréhension, la qualité, l’architecture, les performances etc. (renommer une classe, créer une nouvelle couche, séparer en méthode en plusieurs etc.)
  • Le mot legacy parle un peu moins en général (en tous cas je ne le connaissais pas avant d’être vraiment confronté au problème), pourtant chacun de nous s’est retrouvé ou se retrouvera dans cette situation.

Qu’est-ce qu’un projet legacy ?

Par legacy, on entend projet legacy ou code legacy.
Typiquement, c’est un projet qui n’a été produit ni par vous-même ni par les personnes de votre équipe.

Prenons un cas :
Il y a ce vieux projet qui a été écrit il y a 5 ans (ou plus) avec une techno dépassée par une autre équipe ou même une autre société où le client souhaite des évolutions majeures ainsi que des correctifs de bug.

Depuis, les développeurs de l’époque ont tous fichu le camp et pour couronner le tout, aucune documentation ou aucune spécification de l’époque n’a été laissée pour les suivants, c’est à dire, vous et votre équipe.

Parlons du code, 2 issues possibles :

  • Le code produit a été pris soin par vos prédécesseurs (clair, commenté, facilement maintenable et une couverture de test convaincante).
  • Mais ne rêvez pas, Noël n’a lieu qu’une fois dans l’année et les projets legacy ont plutôt l’allure suivante :
    • un code pas forcément clair ou lisible lors de la première lecture
    • des noms de variables ou des classes peu explicites (abréviations, nom peu clair etc.)
    • des classes énormes (5000~7000 lignes) ou des méthodes trop grosses (500~1000 lignes)
    • du code mort ou dupliqué
    • peu ou pas de commentaires, utiles en tous cas
      • Ne sont pas considérés comme des commentaires utiles :
        • 3 lignes pour expliquer une affectation de variable
        • un TODO qui traîne et qui n’a pas été traité
        • le développeur qui signe son code (ex: // JDU – Fix anomalie #425)
        • du code commenté
    • une architecture complexe et non maîtrisé (il m’est déjà arrivé, dans une architecture 3-tiers, de voir des références de webservice ou des appels en base de données directement dans la Vue…)
    • et enfin, et SURTOUT, pas ou très peu de tests unitaires (car c’est bien connu, tester c’est douter)

On peut dire que ce projet possède une dette technique importante. Or plus la dette technique s’élève, et plus il sera coûteux de maintenir ce projet. Il faut également se dire que la non-qualité entraîne la non-qualité. Difficile de « réparer » une architecture qui déjà bancale depuis le départ.

Les raisons de cette négligence sont multiples : faute de temps, de budget, de compétences etc.

Le problème, c’est qu’en face, votre client les veut absolument ses évolutions et correctifs, et il vous a simplement donné un délai de 3 à 4 mois. Il n’est donc pas opportun de refondre totalement l’application avec des technologies plus récente et en jurant que « oui, cette fois, on va produire un code de qualité ».

Deux façons de procéder :

  1. Dépiler le code au fur et à mesure pour développer la fonctionnalité tout en prenant le risque (réel) de créer des régressions sur une autre partie de l’application. Cette pratique est à éviter surtout si vous n’avez pas d’équipe de testeurs à votre disposition pour vous aider.
  2. Vous appliquez le principe de legacy refactoring, où l’enjeu va être de caractériser le code existant à l’aide de tests unitaires avant de le modifier.
fight_legacy_code_reverse

Poser des tests unitaires sur le code existant

Prenons le cas où vous développez une fonctionnalité (évolution ou correctif).

Ca y est ! Vous avez identifié le bout de code à modifier pour apporter votre fonctionnalité à l’application. Malgré cela, le code que vous voyez mériterait un peu de ménage, autrement dit une bonne refacto.

A ce moment-là, lâchez tout tout de suite ! (sauf le chat qui est sur vos genoux, mais au moins le clavier et la souris). Même si le code à l’écran vous semble avoir besoin d’un grand coup de nettoyage, ce code tourne en production, et pire, il fonctionne… Comment être sûr de ne pas casser le comportement existant et de ne pas entraîner des régressions ?

Avant toute modification de code, il faut caractériser le code par des tests unitaires. Ces tests vont permettre de mieux comprendre le code écrit et surtout, d’être sûr que le code historique fonctionne toujours après vos modifications.

Refactorisez le code legacy

Une fois votre harnais de test sur le code existant en place, vous pouvez refactoriser le code. Ne vous lancez pas non plus dans des chantiers titanestes, vous ne parviendrez pas à refondre toute l’application d’une seule traite. Le legacy refactoring est un long processus, et généralement on se contente d’améliorer uniquement le code que l’on va toucher (sauf si vous avez du temps pour travailler d’autres portions de l’application).

Quelques exemples de chantier que vous pouvez entamer sans problème :

  • Renommer des variables, méthodes ou classes
  • Découper une méthode en plusieurs petites méthodes
  • Revoir une boucle (la transformer en requête Linq si opportun)
  • Sortir les « magic string » ou les « magic int » dans des constantes ou des fichiers de ressources
    • Par exemple, quand vous voyez ceci :
      typeCompte == « A »  || typeCompte == « M »
    • Remplacez par :
      typeCompte == TypeCompte.ADMINISTRATEUR || typeCompte == TypeCompte.MODERATEUR
    • Ou encore
      age >= 18 par age >= Age.MAJORITE

Toutes ces modifications sont possibles désormais, tant que vos tests unitaires ne passent pas au rouge.

Apportez vos modifications en utilisant TDD

Le principe TDD (Test-Driven Development, une autre pratique du software craftsmanship) prône le développement piloté par les tests.

Le principe est le suivant :

  1. Ecrire d’abord le test unitaire qui va valider le code que vous allez écrire
  2. Faire échouer le test (étant donné que votre code n’existe pas encore)
  3. Ecrire le code qui va valider le test
  4. Faire passer le test au vert
  5. Refactoriser

Pourquoi créez vos tests avant votre code et non avant ? Parce qu’une fois votre code écrit sans l’avoir testé au préalable, émergent tout une multitude d’excuses pour passer à côté de l’écriture des tests unitaires : pas le temps, pas de budget, trop compliqué, bug en prod etc.

Parce que la vie d’un projet fait que l’étape des tests unitaires est régulièrement réduite voire supprimée et c’est bien dommage. Pourtant en écrivant bien vos tests de validation avant le code, c’est un nombre non négligeable de tickets de remontées de bug en moins et donc, une phase de recette qui se passe mieux.

Plus d’infos sur TDD :

Pour terminer

En clair, pour apprivoiser le code legacy, il faut s’équiper d’un harnais de tests avant toute modification de code. Ce harnais de test doit être construit au fur et à mesure que vous modifiez le code.

Lors d’un fix ou de l’ajout d’une fonctionnalité, assurez-vous de la développer en TDD afin de compléter votre harnais de tests, car il n’y a rien de pire que des tests unitaires au vert et une application qui ne fonctionne pas.

Pour ma part, j’ai découvert cette notion de legacy refactoring lors de formations proposées. A cette époque, le formateur nous rabâchait : « Le legacy ne doit pas être perçu comme un fléau mais un challenge !« .

C’était difficile à comprendre à l’époque, car le projet sur lequel je travaillais représentait près de 120 000 lignes et 800 jours de dette technique (selon Sonar) ! Cette application avait accumulé une dette technique colossale au fur et à mesure de son existence (initiée en 2003 en ASP.NET 1.1). A la longue, il devenait difficile de maintenir cette application sans entraîner de régressions.

Avec la mise en place des diverses pratiques de software craftsmanship, je commence à mieux comprendre cette phrase, et aujourd’hui nous avons presque réduit le nombre de lignes de code de 10 000 (code mort, dupliqué, complexe etc.) ainsi que la dette technique. Le nombre de tests unitaires, quant à lui, est passé de 0 à 800 en un an pour une couverture de code de 25% en moyenne.

Le chantier est long mais pas impossible.

legacy-dream

Voici un projet sur GitHub pour vous initier au legacy refactoring : https://github.com/sandromancuso/trip-service-kata/tree/master/c%23

Enfin, le livre de référence Work effectively with Legacy Refactoring à commander sur Amazon.

Si cela vous intéresse, j’essaierai de mettre une vidéo en ligne pour vous montrer comment poser ses premiers tests unitaires sur un projet legacy afin de le refactoriser ensuite.

Lien Permanent pour cet article : https://www.jbvigneron.fr/parlons-dev/legacy-refactoring-un-challenge-pas-un-fleau/

2 Commentaires

    • David Bottiau sur 5 mai 2016 à 10h09
    • Répondre

    Bonjour Jean-Baptiste,

    Merci pour ce retour d’expérience intéressant. Une question reste cependant en suspens, comment savoir si cela vaut réellement le coût de continuer à maintenir un code ?

    Prenons le cas d’un projet qui a été développé il y a longtemps et où l’effort a été mis sur la rapidité de développement et non la qualité / les tests / la maintenabilité. Est-il possible de quantitifer le temps que cela peut prendre de :

    – maintenir le code existant sur le long terme (analyse de ce qui a été fait, de comment cela a été fait, de trouver la source du problème ou de la manière dont on doit ajouter la fonctionnalité, le temps de créer la solution au problème/fonctionnalité, etc…)
    – contre celui de repartir de zéro (en TDD, BDD, ou tout autre méthode de gestion de la qualité) afin de réduire drastiquement le temps de maintenance ?

  1. Difficile à dire… tout dépend de la dette technique accumulée au fur et à mesure du projet.. Une première question à se poser : Est-elle absorbable en peu de temps et facilement ?

    Le projet sur lequel je travaillais représentait un chiffre d’affaire très conséquent pour l’entreprise… C’était un peu comme une Roll’s Royce… Pourtant en y regardant de près, peu de développeurs aurait accepté de son plein gré à maintenir ce genre de code.

    Il suffit d’estimer la charge nécessaire pour maintenir l’application tout en prenant compte des risques autour liés à la dette technique, à la non-maitrise du code, aux régressions etc. et de la comparer au prix d’un nouveau projet bénéficiant des dernières technologies. Le choix doit être pris avec consensus, tout en sachant que le second finira par arriver un jour.

    Après tout, quand les réparations d’une voiture commencent à coûter trop cher, on la change, tout simplement 🙂

Répondre à Jean-Baptiste Vigneron Annuler la réponse

Votre adresse de messagerie ne sera pas publiée.

Verified by MonsterInsights