Quelques techniques pour reprendre en main une application legacy C# (première partie)

François Hyvrier
YounitedTech
Published in
7 min readMay 10, 2019

En tant que développeur, il est fréquent que l’on doive maintenir une application dont on n’a pas écrit le code. Souvent les développeurs connaissant l’application sont encore dans les parages et peuvent nous aider à trouver nos marques dans le code, mais ce n’est malheureusement pas toujours le cas.

Dans ce genre de situations on souhaiterait avoir une documentation technique de l’application, mais c’est précisément dans ce genre de situations qu’il n’y en a pas. Lorsqu’on avait expliqué aux camarades développeurs qu’on avait récupéré la maintenance de cette application, leurs mines compatissantes n’étaient déjà pas très rassurantes, mais à présent c’est carrément l’angoisse.

Vient ensuite le moment d’ouvrir le code source de l’application en question, et le nombre de WTFs par minute augmente soudainement : on ne comprend rien au code, ça part dans tous les sens, tous les concepts sont mélangés, il n’y a pas de tests unitaires, l’application n’est d’ailleurs même pas testable avec tous ces appels de méthodes statiques et ces requêtes en base de données en plein milieu de calculs financiers. Le code est un tel sac de nœuds qu’une modification à un endroit peut avoir des conséquences totalement imprévues dans des fonctionnalités qui n’avaient a priori rien à voir avec le changement effectué.

Oh non, du legacy ! Ma vie n’a plus aucun sens et je me sens sale !

Si l’état du code peut déjà être effrayant, il arrive aussi que les processus de livraison et d’exploitation de l’application le soient tout autant : déploiements à base de copier-coller depuis le poste du développeur vers les environnements d’exécution, aucun log applicatif permettant de surveiller les performances ou de diagnostiquer les problèmes. Rien n’est fait pour aider le développeur, tout repose sur des interventions humaines sujettes à erreur et coûteuses en temps.

Il est très difficile et frustrant de travailler dans un contexte où on maîtrise mal ce qu’on fait. Par peur d’introduire de nouveaux bugs, on en est réduit à trafiquer le code pour arriver à ses fins, à chaque livraison la consommation de cierges explose et les tendinites se multiplient à force de croiser les doigts pour que tout se passe bien.

Le refactoring d’applications legacy est un très vaste sujet qui a fait l’objet de beaucoup d’articles (notamment ceux de mes comparses Slim dans son petit guide de survie sur le Legacy code, et Cédric dans la gestion du code legacy chez Younited Credit), et de livres. Le but de cet article est plutôt de présenter quelques techniques que nous avons utilisées pour reprendre en main des applications legacy afin de pouvoir les travailler plus simplement et avec une plus grande confiance.

Automatiser le déploiement

Le déploiement manuel d’applications est une source d’erreurs et une perte de temps. Quelques-uns des logiciels sur lesquels nous travaillons sont des applications console qui étaient déployées manuellement par les développeurs depuis leur poste, cela impliquait de :

  1. Récupérer la dernière version des sources.
  2. Les compiler localement.
  3. Sur la machine cible, créer un dossier portant la date du jour et archiver la version courante de l’application.
  4. Copier-coller les binaires depuis son poste vers la machine cible.
  5. Récupérer le fichier de configuration de l’application depuis le dossier d’archive et le copier dans le même dossier où a été placée la nouvelle version de l’application.

La procédure n’est pas très compliquée mais des erreurs ou des oublis peuvent survenir à chaque étape :

  • Le développeur peut oublier de récupérer la dernière version des sources et compiler une version plus ancienne que prévu.
  • Il peut compiler l’application avec des paramètres inadaptés (debug au lieu de release par exemple).
  • Il peut oublier d’archiver la version précédente, rendant impossible un retour arrière en cas de problème.
  • Il peut oublier de remplacer le fichier de configuration avec la version contenant les bons paramètres.

Ces tâche doivent bien sûr être répétées autant de fois qu’il y a d’environnements, c’est-à-dire quatre fois dans notre cas car nous avons un environnement d’intégration continue, de QA (notre environnement de test), de pré-production et enfin de production. Déployer manuellement l’application sur un environnement est fastidieux, cela dure facilement une dizaine de minutes, et on comprend qu’un développeur soit tenté de sauter des environnements pour gagner du temps.

Automatiser le déploiement est un investissement rapidement rentabilisé. Au prix de quelques jours de travail avec les outils Azure DevOps et Octopus Deploy, nous avons mis en place et configuré les processus de compilation et de déploiement de plusieurs applications legacy.

Du code legacy en cours de livraison.

Les bénéfices sont immédiats : chaque modification de code envoyée au serveur de sources donne lieu à une nouvelle version clairement identifiée par un numéro unique qui est déployée automatiquement sur notre premier environnement d’intégration continue. Les livraisons sur les environnements suivants se lancent ensuite d’un seul clic et durent moins d’une minute. Pour une version donnée, ce sont les mêmes binaires qui seront déployés sur tous les environnements : le code qui sera envoyé en pré-production puis en production sera donc identique à celui qui aura été validé sur l’environnement d’intégration continue et de test.

Les versions (releases) d’une application dans Octopus Deploy et leur état de déploiement.

La configuration de l’application peut éventuellement être différente selon l’environnement, mais dans ce cas il n’est plus nécessaire de se connecter aux machines pour la changer manuellement car les paramètres dont la valeur dépend de l’environnement d’exécution sont automatiquement renseignés par l’outil de déploiement.

La configuration des variables d’une application selon leur environnement (le scope) dans Octopus Deploy.

Un outil de déploiement historise aussi les versions livrées. Grâce à cela on sait faire le lien de façon fiable entre un exécutable déployé et la version du code source correspondant, et en cas de problème il est très simple de redéployer une version antérieure stable.

Détail d’une version (release) d’une application dans Octopus Deploy, avec la référence de la dernière modification de code (commit) à partir de laquelle elle a été générée. On voit qu’elle n’a pour l’instant été livrée que sur l’environnement d’intégration continue (Azure Legacy IC).

Le déploiement automatique sécurise les livraisons en les rendant répétables, crée un lien entre chaque version de l’application livrée (l’exécutable) et la version du code source qui a servi à la générer. Il limite aussi le nombre d’interventions humaines lors des livraisons, et réduit donc énormément le risque d’erreurs tout en faisant gagner beaucoup de temps aux développeurs, temps qu’ils pourront consacrer à améliorer le code de leur application.

Ajouter des traces pour rendre visibles les erreurs de l’application

Pour pouvoir corriger des problèmes, encore faut-il savoir qu’il y en a (c’est fou). En C# les problèmes se manifestent généralement sous la forme d’exceptions indiquant des problèmes très divers : erreur de connexion à une base de données, atteinte des limites de la mémoire de la machine, accès à un index inexistant d’un tableau, etc.

Les exceptions fournissent des informations précieuses sur leur cause et le lieu (la ligne de code) où elles se sont produites, et il est très important de pouvoir les tracer afin de corriger les bugs qui en sont à l’origine.

Quel que soit le type d’applications, il est toujours possible de mettre en place des mécanismes pour tracer les exceptions non gérées (celles qui ne sont pas attrapées par un bloc catch).

Ça peut être un simple bloc try/catch au niveau du point d’entrée d’un programme console (sa méthode main).

Dans le cas d’une application web ou d’une web API, le framework ASP.NET Core permet d’implémenter des middlewares où intercepter et logger les exceptions non gérées :

Définition du middleware de logging pour une application web ASP.NET Core.
Enregistrement du middleware au démarrage de l’application web.

Ajouter ces traces ne demande pas beaucoup d’efforts, le code nécessaire peut la plupart du temps être introduit à un endroit isolé et n’a pas d’impact sur le fonctionnement de l’application, et cela permet de faire la lumière sur ce qui se passe dans le logiciel. La majorité des frameworks fournissent des mécanismes pour intercepter les erreurs et les tracer.

On peut difficilement estimer la quantité et la variété des erreurs qui se produisent dans une application que l’on connaît mal et dans laquelle on travaille à l’aveugle, et le fait de lever le voile sur ces problèmes peut être impressionnant, voire donner le vertige : on ne sait pas par où commencer. C’est dans cette situation qu’un outil d’analyse des logs est très utile : en centralisant les logs, il permet de regrouper les erreurs par type, de faire des recherches ou de calculer des statistiques à partir des données et ainsi aider à mieux comprendre le comportement de l’application et prioriser les bugs à corriger.

Nous utilisons par exemple beaucoup la solution Application Insights disponible dans Azure : son intégration est très simple (il suffit d’installer un package NuGet et de renseigner une clé d’API) et possible pour tous les types d’applications. Les données collectées sont accessibles dans une interface du portail Azure qui permet de les analyser sous toutes les coutures.

Synthèse des exceptions d’une application dans l’outil Application Insights.

Visualiser les erreurs d’une application n’est pas seulement utile pour découvrir les problèmes existants, cela permet aussi de détecter plus rapidement l’apparition de nouveaux bugs introduits lors des modifications du code source, et ainsi les corriger au plus tôt.

Cette première partie avait pour but de montrer que reprendre une application en main passe aussi par la mise en place d’outils permettant de fiabiliser son exploitation et de voir les problèmes qui surviennent à son exécution. Ce travail est bien un peu fastidieux, mais il ne présente pas de difficulté particulière. Ses résultats sont immédiats et très précieux car ils permettent de gagner beaucoup de temps (en déploiement et en diagnostic), de mieux comprendre ce qui se passe dans l’application et ainsi la corriger plus efficacement.

La seconde partie de cet article présentera une façon de transformer une section de code legacy pour la rendre testable.

--

--