legacyAI 03

Travailler avec le “Legacy Code”

Par Mikaël Hubert-Deschamps – mars 2024

En janvier 2024, j’ai eu la chance de co-présenter l’évènement mensuel des Techno Drinks à nos bureaux de KuriosIT à Sherbrooke. La thématique de la soirée portait sur les techniques de travail avec du legacy code, soit le code patrimonial ou hérité en français. La présentation est passée à travers plusieurs améliorations que l’on peut apporter à ce genre de projet, autant sur la syntaxe du code, sur sa structure ou encore sur les méthodes de mise en production. Cet article de blogue fera un tour d’horizon des points mentionnés lors de cette conférence.

Avant de débuter, je tiens à souligner l’excellent travail de Michael C. Feathers pour son ouvrage Working Effectively with Legacy Code paru en 2004. Ce livre donne une définition claire de ce qu’est du legacy code. Il présente des méthodes de refactorisation selon l’objectif souhaité, une multitude de mises en situation où l’auteur explique étape par étape les possibles solutions et différentes approches pour ajouter des tests unitaires. Je recommande la lecture de cet ouvrage à quiconque souhaite en apprendre plus sur les méthodes de travail avec du vieux code!

Mise en situation

Dans le cadre d’un projet en continu avec un de nos clients, j’ai eu à supporter plusieurs applications conçues en C#, plus spécifiquement en WinForms .NET Framework 4.7.2. Ces applications ont toutes plusieurs années d’utilisation, certaines d’entre elles avaient (et ont toujours) besoin de support afin d’ajouter de nouvelles fonctionnalités ou pour régler des bogues existants. Je considère ces applications comme legacy car dans mon cas, cela se rapporte beaucoup plus à une application sans documentation.

Que ce soit par le biais d’un readme, par des commentaires dans le code, ou encore par l’intermédiaire de l’expérience d’utilisation du client, nous n’avions réellement aucune information disponible autre que le code source. De plus, aucun gestionnaire de sources (i.e. GitHub) n’avait été utilisé pendant le développement des applications. Il a donc fallu s’asseoir, se servir du café et… lire le code!

Après neuf mois à me plonger dans ces applications, j’ai redressé la structure de la plus importante des multiples applications que nous avons pris en charge du client. J’ai aussi ajouté plusieurs fonctionnalités et ramené à la vie certaines parties qui n’étaient plus supportées. Voici donc tous les changements effectués à l’application principale.

Nouvelles fonctionnalités

Dans ce projet, l’objectif était de livrer de nouvelles fonctionnalités tout en refactorisant l’application. Au tout début de la prise en charge, même l’ajout et la modification des usagers était compliquée pour l’administrateur: il fallait se connecter directement à la base de données et modifier le contenu des tables! Le premier ajout a donc été une interface de gestion des utilisateurs. Par la suite, nous avons remis sur pied plusieurs fonctionnalités qui, par manque de maintenance, étaient hors-service depuis plusieurs années. Tous ces ajouts (et d’autres encore) ont permis au client de pouvoir utiliser son application exactement comme il voulait, et ce, sans nécessiter notre support pour chaque opération.

Processus de mise en production

Pour la mise en production de l’application, un fichier exécutable était auparavant produit et déposé dans les serveurs de fichiers de l’entreprise avec toutes les librairies et autres fichiers requis. Cette méthode apporte plusieurs problèmes: impossibilité de mettre en production par nous-même en raison du manque d’accès, application indisponible si le serveur est hors-service, fichiers de configuration visibles et modifiables par l’usager, etc.

Bref, cette façon de faire n’est pas une bonne pratique du tout, surtout quand de meilleures options s’offrent à nous. J’ai donc utilisé Wix pour générer un fichier d’installation .msi et ainsi déployer l’application directement sur les postes des usagers. Terminé les erreurs de latence aléatoires causées par le serveur de fichiers et les bogues mystérieux de fichiers ou configurations manquants. Nous avons repris le contrôle de l’environnement de l’application!

Tests unitaires

Rappelons-nous, une application sans tests est considérée legacy ! Nous avons donc fait l’ajout de quelques tests unitaires qui valident les différentes fonctionnalités des services. Ces tests ont permis de résoudre plusieurs bogues que nous n’avions pas trouvés pendant les tests fonctionnels. Ces tests ont donc été rentabilisés avant même leur ajout en production.

Refactorisation

Afin d’allonger la durée de vie des applications, j’ai effectué beaucoup de refactorisation. Au départ, la structure était monolithique: un formulaire (WinForms, bien entendu) principal contenant toute la logique et plusieurs enfants échangeaient l’information avec le parent. La classe principale comptait plus de deux mille lignes, dont de nombreuses n’étaient plus utilisées. J’ai dû retirer tout le code mort et clarifier les noms de variables.

Par ailleurs, j’en ai profité pour migrer l’application principale vers .NET Core 8. Cette amélioration seule apporte beaucoup d’optimisations et de sécurité à l’application.

Après ces changements, même si le code est plus facile à lire, nous avons encore un monolithe… J’ai donc créé des services injectés dans le formulaire principal pour mieux traiter les différents objets de l’application. Par exemple, si une action modifie les informations d’un employé, ces changements vont maintenant passer par le service associé centralisé. Fini les duplicatas!

Ensuite, j’ai enclenché un énorme nettoyage du code en réécrivant des segments qui n’étaient clairement pas optimaux (voir exemple plus bas). Finalement, j’ai regroupé l’ensemble des requêtes SQL sous un seul fichier responsable des communications avec la base de données. C’est pendant cette étape qu’un grand nombre de bogues, qui rendaient l’application inutilisable, ont été trouvés et réglés.

Parlant de requêtes SQL, nous avons aussi fait l’intégration de Dapper afin de faire le lien entre les objets relationnels et ceux de notre domaine applicatif. Cet ajout a permis d’uniformiser la désignation des propriétés et d’optimiser les transactions. La configuration de cet outil est très simple. Le temps que l’on sauve à l’utiliser et le gain en sécurité (comme les protections contre l’injection de code) sont considérables!

Finalement, nous avons aussi fait la mise à jour de l’ensemble des librairies externes utilisées. Certaines d’entre elles comportaient de graves problèmes de sécurité. Il était donc impératif d’en faire la mise à jour.

Voici un exemple de segment de code à optimiser:

Supposons ‘foo’ une valeur logique (boolean) et ‘bar’ une chaîne de caractère (string). (Cet exemple est réellement tiré du code original).

switch(foo)
{
    case false:
        bar = "foo est faux"; // pas besoin d'un switch case
        break;
    case true:
        bar = "foo est vrai"; // un opérateur ternaire fonctionne aussi
        break;
}

Dans cet exemple, le programme utilise un switch case pour déterminer ‘bar’ selon ‘foo’. En réalité, un switch case sert à choisir entre plus de trois cas et le paramètre en entrée est normalement d’un autre type que booléen. La méthode montrée fonctionne, mais n’est pas optimale. Un opérateur ternaire est donc plus approprié puisque seulement deux cas sont possibles.

Voici la version optimisée remplaçant le bloc précédent:

bar = foo ? "foo est vrai" : "foo est faux"; 

Conclusion

Maintenant, on peut souffler: le code est clair, tout ce que l’on voit est utilisé, optimisé et les classes sont structurées. En bref, tous ces changements sont individuellement triviaux, mais regroupés ensemble, ils forment une arme redoutable contre le legacy code. En quelques mois, nous sommes passés d’une application fragile et monolithique à un outil de travail performant, stable et flexible. Le prochain développeur qui ne connaîtra pas l’historique de l’application pourra plus aisément prendre contrôle du code. Il pourra lire la documentation et les tests, consulter les différents services injectés déployer une nouvelle version en un claquement de doigts!

Vous êtes curieux de faire d’une de vos applications un outil performant, stable et flexible? Je vous invite à lire le livre de Michael C. Feathers et à en apprendre plus sur le sujet que nous avons survolé ensemble.