Oct

Hériter d’un projet ancien n’est pas exactement le rêve de tout développeur. Que ce soit une application historique de l’entreprise, un ancien projet issu d’un rachat, ou simplement un site web mal géré par nos prédécesseurs, le code dit legacy est souvent vu comme une corvée voire une punition.

Mais ce genre de projet peut être au moins aussi intéressant et épanouissant qu’un projet “tout neuf”, et devenir un vrai challenge qui va titiller notre curiosité technique. Nous allons voir ensemble comment appréhender un tel projet, les challenges et les phases par lesquelles l’équipe va passer, et quelques méthodes de travail possibles pour s’en sortir.

Un projet legacy, c’est quoi ?

Commençons par définir clairement ce qu’est un projet legacy. Cela peut être vu différemment selon les personnes et les équipes, et chacun a sa propre définition.

  • Un projet legacy est du vieux code dans un langage oublié
  • Ou simplement dans un framework ancien qui n’est plus maintenu
  • Un projet hérité d’une autre équipe sans aucun référent restant
  • Cela peut être un projet qui ne comporte aucun test, aucune documentation, ou difficile à tester
  • Un projet avec une grosse dette technique
  • Une application avec de mauvais process (code spaghetti, patterns mal utilisés, …)

Plus généralement, on peut s’accorder à dire que du code legacy est du code qui a toujours de la valeur, probablement utilisé en production et qui fonctionne, mais difficile à appréhender et qui demande globalement un effort significatif pour être maintenu.

Réécrire ou pas ?

On considère donc qu’une base de code legacy a toujours de la valeur. On pourrait être tenté en première instance, étant donné l’aspect inmaintenable du projet voire l’absence de tests, de repartir de zéro pour “faire propre”. Les questions qui se posent alors sont :

  • D’une part, va-t-on faire mieux ? 🙂
  • d’autre part, quelle est la valeur ajoutée à cette réécriture ? Les ressources ne pourraient-elles pas être utilisées autrement ?

Réécrire un projet doit être réservé à des cas très particuliers, si le projet n’est par exemple pas fonctionnel, s’il s’agit d’un tout petit projet, ou si le projet est amené à grossir encore et donc la réécriture pourrait valoir le coup sur le long terme, épargnant de douloureuses et risquées mises à jour du code. Dans les autres cas, il faut apprendre à travailler avec, et nous allons voir comment dans les parties suivantes.

Quelques challenges auxquels s’attendre

Manque de documentation

On en parlait dans la première partie, les projets legacy ont souvent peu de documentation disponible. Et quand ils en ont une, elle a souvent le défaut d’une grande partie des documentations de projet : le manque d’exhaustivité.

Une équipe qui travaille sur un projet sur une longue période aura tendance à trouver certains points “évidents” et omettra de les détailler dans la documentation, quand une équipe totalement nouvelle sur le projet, parfois sans personne référente pour l’aider, trouvera que ces points manquants sont essentiels. Un peu de rétro engineering peut être nécessaire dans tous les cas.

Librairies propriétaires

Cela peut arriver lors de rachats par exemple, l’équipe d’origine utilisait les librairies propriétaires pour diverses briques du projet (endpoints d’API, accès base de données, etc) et ces librairies ne sont pas comprises dans le rachat. Il faudra donc les analyser et les remplacer par des librairies d’accès libre, ou bien par le framework de la société cible.

Cela peut être source d’erreurs qui ne seraient découvertes que lors des tests manuels de l’application, ou pire, lors du développement de nouvelles fonctionnalités, des semaines plus tard, voire à la mise en service. C’est un point critique à ne pas négliger.

Code mort

Le projet était peut être déjà considéré comme legacy avant d’arriver entre nos mains… et certains blocs de codes ou anciennes fonctionnalités n’ont pas été maintenus, ni supprimés. Cela peut être déroutant quand on croit disposer d’une fonctionnalité sur une application, avant de se rendre compte qu’elle ne marche pas, ou bien qu’elle fonctionne peut-être mais n’est appelée nulle part.

Cela complexifie inutilement le projet et rend son analyse plus difficile. Il n’y a pas grand-chose d’autre à faire qu’à éliminer des blocs de code dès qu’on est certain qu’ils sont inutiles, et cela peut parfois arriver très tard dans le projet.

Dépendances out-of-date

Dans la même veine que la suppression du code inutile, les dépendances sont autant que possible à maintenir à jour pour profiter des derniers bugfix et update de sécurité. J’expliquais dans un autre article : Cure de jouvence des dépendances, pourquoi il était primordial de tenir les dépendances de son projet à jour, et c’est d’autant plus vrai sur un projet legacy à la dette technique déjà élevée, même s’il peut être tentant de ne toucher à rien “tant que ça fonctionne”. 

Le coût d’une mise à jour des dépendances augmente avec le nombre de versions majeures en retard, et il peut devenir considérable et difficile à mettre en œuvre le jour où on n’a plus d’autre choix.

Quelques étapes clés dans la prise en main du projet

Nous allons voir 3 phases essentielles pour s’approprier un projet legacy. La première phase est un temps de lecture et d’analyse, le second consiste à faire fonctionner le projet, et enfin la communication de ces informations à toute l’équipe.

Phase de lecture de code

Evidemment, on commence par ouvrir la documentation si on a la chance d’y avoir accès, et la base de code pour regarder ce qui s’y trouve : l’architecture globale, la version du framework et des dépendances (et à quel point celles-ci sont maintenues), les accès à des services tiers.

Cette tâche permettra d’obtenir un rapport préliminaire de l’état du projet, et de ce qu’il est possible de faire. Elle sera idéalement menée par des développeurs seniors, qui seront mieux à même de comprendre les décisions prises dans le contexte historique du projet, et seront probablement plus compétents dans la technologie dans laquelle il est écrit.

C’est un bon point de départ pour guider les choix à faire et les tâches à prioriser concernant la suite de la reprise du projet.

“Faire marcher” le projet : suite de l’analyse

Lire du code ne permettra pas de pleinement apprécier le projet : il est nécessaire de faire fonctionner le projet sur un environnement de développement. 

Tester en direct l’application permettra d’une part, d’aider à lister les cas d’utilisation, de s’approprier le fonctionnel, et d’appréhender au mieux toutes ses possibilités, et d’autre part à mieux cerner les dépendances et accès aux services tiers, qu’il faudra peut-être mocker, voire à terme, remplacer.

Une fois que le projet tourne, c’est le moment pour profiter des outils d’intégration continue qui permettront d’automatiser le build et le déploiement du projet. Garder l’outil dans le vert rassurera toute l’équipe projet et permettra d’identifier très rapidement les problèmes qui peuvent survenir lors de la modification du code legacy.

Documenter et partager

Une fois la procédure d’installation effectuée, il sera profitable à toute l’équipe d’expliciter et/ou simplifier les différentes étapes, qu’on aura probablement suivi dans des documentations complètement dépassées, ou dans le pire des cas, déduit de la base de code seule…

Ce peut aussi être le moment d’ajouter quelques tests fonctionnels s’ils sont absents. Ils permettront d’éviter les régressions par la suite, et dans le même temps de valider la compréhension fonctionnelle du projet, de ses cas limites et zones critiques, tout en servant de documentation.

Des pistes d’attaque

Découpage d’un monolithe en modules

Une première approche possible dans le cas d’un monolithe, est d’essayer de le découper en modules plus petits. Cela est pertinent si on sait qu’on aura besoin de faire des modifications sur le projet par la suite, en particulier sur les modules qu’on aura créés. Une couverture de tests convenable, notamment composée de tests fonctionnels de bout en bout, est évidemment un prérequis à ce processus pour écarter toute régression.

Selon l’organisation initiale du projet, on essaiera d’identifier des “couches” par exemple sur un modèle n-tier, ou des “blocs” qu’on peut rendre indépendants comme sur un projet micro-services. L’objectif sera de les isoler pour rendre nos nouveaux modules moins couplés entre eux tout en respectant le principe de séparation des responsabilités.

Cela rendra la documentation, les tests, et le partage de tâches plus facile dans l’équipe, et réduira les risques d’erreurs dans tout le projet lors des modifications sur les modules. La dette technique sera aussi plus facile à maîtriser sur de petits modules que sur le monolithe initial.

Isolation / Anticorruption layer

Dans le cas où on a un projet legacy mais fonctionnel, voire exploité en production, isoler le code ancien va permettre d’ajouter des fonctionnalités si cela est nécessaire, tout en préservant l’intégrité du projet et réduisant les risques d’erreur.

Une Anticorruption Layer (ACL) est une couche applicative qui va faire le lien entre l’ancien et le nouveau code. Elle aura à la fois le rôle de façade entre les deux interfaces, un rôle d’adaptateur entre les deux systèmes, et enfin le rôle de traduction des données.

Ainsi le code legacy reste isolé voire inchangé, et le nouveau code n’a pas à utiliser les interfaces et le modèle de données (qu’on pourra juger inapproprié ou déprécié).

Pas les ressources pour une réécriture ?

Nous avons vu plusieurs manières de procéder quand on doit travailler avec, voire faire évoluer, un projet legacy. Mais il y a un cas que nous n’avons pas abordé, celui où un projet entre dans la catégorie de projet où il serait pertinent de procéder à une réécriture, mais que les ressources ne sont pas disponibles pour l’instant. Il faut “faire avec” le projet legacy “en attendant”. Comment travailler intelligemment sur un tel projet ?

Se projeter pour être efficace

On pourra utiliser les étapes décrites précédemment : analyse, tests, partage, en se projetant sur une réécriture. Le coût d’écriture des spécifications du nouveau projet est bien plus abordable que la réécriture elle-même. Cela permettra d’avoir une idée précise d’où on désire aller, obtenir une base d’architecture cible, que l’on utilisera comme référence pour la mise en œuvre des modifications. Cette mise à plat du besoin sera gage de sérieux et pourra aider l’équipe management à envisager plus facilement la refonte du projet. Les nouvelles fonctionnalités seront implémentées dans le nouveau système, et les fonctionnalités legacy seront migrées une par une, dès que ce sera possible.

Ainsi, si on veut aller par exemple sur une architecture microservices, on focalisera l’effort de refactoring sur la division progressive en petits modules. On pourra commencer par ceux qui nous concernent pour les premières modifications à effectuer, et/ou les fonctionnalités legacy les plus importantes à migrer, en se calquant sur l’architecture cible.

Un coût qui vaut le coup

Les deux systèmes vont coexister sur toute cette période de transition. Fatalement, cela aura un coût, mais cela prépare le terrain pour le moment où les ressources seront allouées pour terminer la réécriture totale du projet, tout en permettant de continuer d’exploiter le projet en production.

Conclusion

Appréhender un projet dit legacy est délicat pour une équipe qui part souvent de zéro, mais nous avons vu plusieurs points sur lesquels se focaliser pour faciliter le travail.

La gestion du code ancien est primordiale : l’ isoler et le nettoyer seront les priorités, si possible en toute sécurité en se protégeant des régressions par une belle batterie de tests.

Le nouveau code quant à lui, sera maintenu et testé minutieusement. Les dépendances seront autant que possible mises à jour. Les nouvelles fonctionnalités et modifications indispensables de l’ancien code seront appréhendées intelligemment.

Enfin, le projet sera progressivement refactoré et documenté de manière appropriée afin de ne plus revivre ça 🙂

Carole CHEVALIER

Related Posts

Leave A Comment