Une réflexion sur la réécriture de projet et la dette technique

Réécrire ou ne pas réécrire ? Telle est la question.

Ce billet est une réflexion suite à un ènième débat sur la notion de réécriture de projet et de dette technique, et c’est parti sur du tellement long que je me suis dit que je devrais en faire un billet séparé.

À propos de la dette technique dans un projet

J’en ai vu passer des projets avec de l’historique dans ma vie (dont un avec du code et des données de plus de 40 ans) ; et quoi que tu décides, quoi que tu fasses, la simple longue existence du projet va amener à des choix compliqués, surtout quand il s’agit d’exister face à de nouveaux arrivants ou des concurrents (au sens large) qui ont beaucoup plus de moyens.

Le simple fait que le projet vive fait qu’il va vieillir, par rapport aux nouvelles (et sans cesse renouvelées) façons de faire. Les outils changes, les besoins changent, les contraintes changent (pensez aux contraintes CPU, mémoire, I/O, disque… d’un projet il y a 40 ans ; le matériel moderne n’a absolument plus rien à voir), les exigences utilisateur et de sécurité changent.

Le point le plus critique là-dedans, de ce que j’ai vu personnellement (votre expérience peut être différente), c’est les outils au sens large. Les frameworks, les langages eux-même évoluent et les versions anciennes voient progressivement leur support disparaitre.

Les possibilités (je n’ose pas parler de « choix ») classiques sont :

1. Ne rien faire, ou presque

On s’arrange pour faire le minimum vital pour que ça tourne encore en production sur l’existant, et on met le gros des ressources sur les nouvelles fonctionnalités. On laisse donc filer la dette technique et ne corrige que le minimum pour garder la tête hors de l’eau. Ça a l’air con dit comme ça, mais dans l’industrie c’est un choix très fréquent, parce que la maintenance et la mise à niveau, ça coute cher et ça ne se vends pas au client, qui a souvent beaucoup de mal à comprendre pourquoi il paierait pour une nouvelle version qui fait la même chose (ou moins bien), avec potentiellement de nouveaux bugs, alors que « l’ancienne marche ». À ce sujet il y a beaucoup d’éducation des clients à la réalité du monde informatique, surtout sur les projets ouverts sur Internet, mais c’est encore un autre débat.

Ça peut fonctionner comme ça assez longtemps, en réalité. Jusqu’à la catastrophe, en général une technologie clé totalement à l’abandon, qui nécessite un effort démesuré pour être remplacé. Exemple : il y a 40 ans vous vous êtes beaucoup reposés sur Pro*C ou Progress 4GL et objectivement c’était à l’époque une excellente solution, peut-être la seule capable de tenir la charge avec le matériel disponible. Maintenant, la technologie est à l’abandon, et les seules personnes encore capables de comprendre votre base de code métier critique s’approchent de la retraite.

À propos : c’est très facile de juger à postériori qu’un projet a été mal géré, qu’un choix technologique est mauvais. C’est beaucoup, beaucoup plus complexe sur le coup – voire être complètement faux. Avant de cracher sur les personnes qui ont travaillé historiquement sur le projet (un sport très commun), rappelez-vous qu’une choix technologique ou de gestion aujourd’hui mauvais pouvait très bien être, au moment de la décision, un bon choix, peut-être le meilleur disponible, à cause de contraintes différentes dont vous n’avez sans doute même pas idée. Ne faites pas ça : ça ne sert à rien, le passé est le passé, vous n’allez pas le changer en méprisant des gens.

2. Maintenir la base de code à peu près à jour, en permanence, tout le temps

On limite donc en permanence la dette technique sur le projet. Ça nécessite un gros effort continu, et assez de ressources (de tous types) pour y arriver, surtout si on veut continuer à évoluer, proposer des nouveautés en même temps.

C’est un choix qui est assez rarement fait, mais qui est tout à fait tenable avec des équipes bien organisée ; son principal problème est son pré-requis absolu : tous les directeurs techniques du projet, doivent tous et toujours garder cette priorité, et s’assurer d’avoir assez de ressources en permanence pour y arriver. Le moindre trou dans la raquette ramène très vite au cas 1, avec une dette technique irrattrapable.

Ça implique d’avoir des briscards du développement à la tête du projet, des gens qui savent exactement quoi faire et quand pour le bien du projet, et qui ont les moyen d’imposer des priorités de maintenance quand c’est nécessaire ou de refuser des « améliorations » qui vont poser trop de problèmes à long terme. Ce genre de ligne est difficile à tenir, et fonctionne surtout sur des projets libres avec un leader fort. Il implique aussi de choisir des technologies stables dans le temps, donc pas de truc trop propriétaire, ni exotique, ni le dernier framework JS à la mode (ou d’avoir des équipes massives pour refaire des choses en permanence).

3. Tout casser et tout recommencer (la fameuse réécriture de zéro)

Idéalement ça devrait être la solution à ne choisir que quand les deux premières ont échouées ; en pratique on aime beaucoup réécrire pour de mauvaises raisons. Les gens sous-estiment massivement la difficulté et le cout d’une réécriture d’un projet, surtout s’il est un peu complexe. J’en ai vu, sur des petits projets, qui se sont bien passées ; mais aucune n’a tenu les charges et délais estimés au début.

D’autre part, il y a des cas où la réécriture est objectivement un bon choix, même sur un projet énorme. Une base de code trop mal conçue, trop mal maintenue trop longtemps, qui se repose sur des technologies complètement obsolètes… et l’effort de remise à zéro peut dépasser celui d’une réécriture tout en imposant une forte charge de maintenance en permanence pour le futur.

En fait, il y a deux sous-cas dans cette fameuse réécriture.

  1. Le projet est petit et simple (disons, un site web vitrine). Là c’est facile. Oubliez que c’est « une réécriture » ; en fait c’est un tout nouveau projet qui ne va rien récupérer de l’ancien, sauf par coïncidence. Ça peut très bien se passer.
  2. C’est un gros projet. Et là ça, quelle que soit la raison de la réécriture – aussi bonne soit-elle –, quels que soient les moyens employés, ça va être très long et douloureux. Bon courage.

Mais alors, comment faire avec ce projet historique ?

La solution idéale dans un monde parfait est généralement la 2, « maintenir la base de code à jour en limitant en permanence la dette technique ».

Sauf qu’on est pas dans un monde parfait mais dans le monde réel ; que vous pouvez déjà faire face à un projet quasiment irrécupérable quand vous arrivez dessus ; que vous aurez du monde à convaincre pour cette histoire de maintenance qui n’apporte rien au « client ».

Alors, on fait quoi ?

Je n’ai pas de réponse absolue à cette question, mais voici quelques pistes issues de mon expérience.

Communiquez au sein des équipes

Les personnes en charge des ressources doivent savoir ce que va leur couter la maintenance, mais connaitre le cout de l’absence de maintenance. Ils vont râler, mais moins que quand leur produit sera ignoré par tout le monde en faveur d’une concurrence plus à jour et plus fiable bien qu’ayant moins de fonctionnalités.

Parlez aux gens en utilisant leur langage. Vous avez un responsable technique en face de vous ? Parlez de mise à jour de framework, de technologies. Un responsable sécurité ? Mentionnez les failles à corriger, et l’impossibilité de le faire si les outils ne sont pas à jour. Un responsable financier ? Mentionnez les couts de maintenance et ceux des pertes dues à l’absence de maintenance (contrats perdus, etc). Un commercial ? Donnez-lui des billes pour vendre cette maintenance, à base de qualité de service par exemple.

N’oubliez pas non plus que personne n’aime faire de la maintenance idiote sur un projet obsolète. Un projet à jour, c’est des équipes motivées et plus productives, avec moins de risque de partir voir ailleurs.

Mettez les utilisateurs dans la boucle

Les utilisateurs savent ce dont ils ont besoin. Pas vous.

Donc, mettez les utilisateurs dans la boucle, recueillez leur besoin, leur avis, leurs retours ; faites des sessions de démonstration, trouvez des utilisateurs prêts à essuyer les plâtres de versions expérimentales, et surtout, écoutez-les.

Bien sûr, il ne s’agit pas de dire amen à toutes les demandes de tous les utilisateurs (c’est une garantie pour avoir une usine à gaz), mais il devrait être assez facile de comprendre ce qui est vraiment indispensable, ce qui est utile et ce qui est superflu bien que ça semblait être une bonne idée à l’origine.

D’autre part, un utilisateur qui discute avec vous, qui sait comment le logiciel est créé, sera plus à même à comprendre les besoins de maintenance, les versions qui n’apportent presque que des mises à jour technique, pourquoi cette évolution qui a l’air simple de son point de vue ne l’est pas du tout.

Dans le domaine, sortir du clientélisme pour venir à une relation de partenaires de confiance permet à tout le monde d’être gagnant, sauf peut-être le commercial qui est récompensé aux profits immédiats.

Accompagnez le changement

La communication est d’autant plus importante en cas de réécriture : à moins d’avoir des moyens colossaux, une réécriture, c’est l’abandon d’une version « qui marche », la mise en pause de nouvelles fonctionnalités pour longtemps, des régressions, des fonctionnalités qui disparaissent…

Aucun utilisateur n’aime ça, et c’est normal.

La bonne nouvelle, c’est que les techniques d’accompagnement au changement existent depuis longtemps et fonctionnent bien. C’est du travail, ça ne va pas être facile, ça sera simplifié s’il y a une relation de confiance avec les utilisateurs (cf plus haut), mais c’est toujours indispensable.

Faites dans le stable et classique

Bon, d’accord, ce nouveau langage / framework / outil a l’air très cool. Mais il n’est éprouvé par personne et est développé par une personne dans son garage. Avez-vous vraiment envie de baser des composants critiques sur un socle qui aura peut-être disparu dans deux ans ? C’est encore pire si l’outil est produit par une start-up, parce qu’en plus vous n’aurez même pas le code source pour maintenir cette brique indispensable vous-mêmes, mais uniquement vos yeux pour pleurer.

Alors oui, c’est sans doute moins amusant de rester dans ces langages classiques et outils un peu dépassés au lieu d’être à la pointe de la technologie. Mais ici on parle de projets au long court, pas de projets jouets, de code jetable ou de R&D.

Découplez !

Oui, ça a l’air con dit comme ça, mais c’est un problème ultra-courant dans la vraie vie. Surtout, il y a plus de chance de réutiliser du code, de changer d’outils sans tout casser si chaque partie est soigneusement découplée dans son propre coin, la plus indépendante possible du reste.

D’autre part : mettez en place un outillage pour forcer ce découplage. Parce qu’un projet à 150 modules censés être indépendants mais qui contient des milliers de dépendances cycliques n’est pas seulement devenu un gros blob. C’est surtout devenu un projet qu’il n’est plus possible de redresser en gardant un effort correct.

Testez !

Pour des raisons évidentes. Avec un code bien testé, on peut plus facilement se lancer dans des changements majeurs sans craindre de tout casser avec un déluge de régressions.

À ce propos : pensez aussi à découpler vos tests du code de run. Ça implique d’avoir des tests d’intégration, des tests fonctionnels en plus des tests unitaires. Ces outils de tests plus-ou-moins-fonctionnels intégrés aux frameworks peuvent être très pratiques, mais induisent un couplage fort, et vous vous retrouvez avec une batterie de tests complètement inutiles (et à réécrire) si vous voulez changer de framework…

Vous n’imaginez pas la quantité de projets que j’ai croisés qui n’avaient pas la moindre ligne de test – ou des tests qui fonctionnent uniquement sur la machine du développeur principal.

Documentez !

Là aussi ça a l’air con. Mais d’expérience, un énorme problème lors de la reprise de maintenance ou de la réécriture, c’est précisément l’absence de documentation. Et à ce sujet, je l’ai tellement entendue et elle m’énerve que je la met en rouge avec des grossièretés dedans :

Le code n’est pas une putain de documentation. Jamais.
« Regarde comment c’est fait aujourd’hui » n’est jamais une réponse valable à une question qui porte sur le fonctionnel.

Un code lisible est très pratique – disons même « indispensable » – pour aller retrouver un détail d’implémentation ; et surtout pour garder le projet maintenable.

Mais jamais, au grand jamais, le code ne pourra servir de référence pour la documentation fonctionnelle du projet ; parce que même si on arrive à faire « facilement » (ça n’est jamais le cas) le lien entre le code et le besoin fonctionnel, rien ne dit si tel ou tel comportement étrange est volontaire, ou un bug dans un cas aux limites. Or, quand on veut reprendre des pans entiers de code (pour cause de maintenance lourde ou de réécriture), le premier point c’est de conserver la cohérence fonctionnelle du code.

Alors, sortez-vous les doigts et écrivez au moins une documentation sur ce que le projet devrait faire d’un point de vue fonctionnel ! Embauchez des petits jeunes pour ça si besoin. Ça existe : une de mes missions en stage, c’était littéralement de réécrire une documentation à partir du code.

Sachez abandonner

Parfois, un projet n’est plus viable, et il faut l’abandonner, jeter plein de travail à la poubelle. C’est d’autant plus difficile qu’on a beaucoup investi dedans, et qu’on a l’impression qu’il ne faudrait « pas gâcher » en continuant coute que coute.

Ce biais, malheureusement pas assez connu, porte le nom de couts irrécupérables, et vous pouvez en savoir plus sur la page Wikipédia ou via cette excellente vidéo de Science Étonnante.

Ce que ça dit surtout, c’est que parfois, la meilleure solution consiste à mettre du travail à la poubelle. Que ce soit un projet trop ancien et trop mal en point qu’il faudra réécrire complètement ; ou une réécriture qui se passe mal et va dans le mur. Dans tous les cas, parfois, mieux vaut sacrifier du travail déjà fait que persister à vouloir rendre viable ce qui ne l’est pas.

N’hésitez pas à parler explicitement du biais des couts irrécupérables autour de vous : le connaitre, c’est déjà le premier pas vers son identification et donc son contournement.



Oui, beaucoup de réécriture de zéro ne sont pas justifiées.

Oui, parfois c’est intéressant, objectivement, de tout casser pour tout réécrire.

Oui, travailler sur un projet historique peut être passionnant (déjà parce que c’est souvent du gros projet avec un fonctionnel intéressant, et des défis techniques uniques).

Non, un projet n’est pas condamné à devenir un blob de n’importe quoi ni à se faire réécrire tous les trois mois. Mais ça demande de la méthode.

Je vous ai présenté ici quelques pistes issues de mon expérience personnelle ; n’hésitez pas à ajouter les vôtres en commentaires.


Icône : CC BY-SA d’après « Anciano », peinture à l’huile d’Ulpiano Checa (1860–1916) ; numérisation CC BY-SA Poniol.

4 commentaires

J’avoue qu’aussi bien le commentaire sur linuxfr que ce billet me laissent un goût de "oui mais" et je déteste cette sensation. Pour éviter de faire de la "fausse contradiction", je vais juste décrire mes trois expériences dans le cadre de "on recommence de 0" en essayant d’en tirer quelque chose. Peut être qu’on aura des conclusions identiques, peut être pas, je les ai pas encore écrite.

Projet 1 : Monolithique -> (Presque) micro service

C’est l’histoire d’un SaaS qui prenait de l’embonpoint, qui pour des raisons commercial allait devoir être découpé (une partie de ce SaaS avait été acheté par un client pour un "cloud privé" sans que le reste ne soit concerné).

Le projet a commencé avant que je ne sois embauché, j’ai connu ses débuts en tant qu’ingénieur QA. Et autant dire que c’était pas de la tarte.

un peu comme dans le cas "Passage à Wayland" le fait d’extraire le module qui avait été acheté indépendamment pour y mettre une version découplée et plus moderne (ça parle de stockage objet, de redondance avec PRA automatisé, plutôt qu’un simple montage NAS) a pas mal géré de soucis : forcément avant on avait un couplage fort, maintenant on a :

  • Application principal
  • Nouveau module
  • Nouveau service de stockage
  • Connecteur entre application principale et nouveau module
  • Connecteur entre nouveau module et service de stockage

Et tout ce beau monde avait du mal à s’en sortir sans bugger, j’ai mis mon véto en tant qu’ingénieur QA. Alors la boîte a dit "OSEF de la QA", elle a aussi remboursé plein de clients qui avaient un déni de service quand le nouveau module a été poussé. Puis ça a été la course pour tout merger.

Quelques éléments que je retire :

  • il existe des bonnes raisons, qui ont une grande valeur tant fonctionnelle (PRA) que commerciale de faire du "on refait de zéro (ou presque)"
  • Un tel projet doit être vraiment traité avec toutes les parties prenantes : dev, QA, mais aussi clients de l’existant et clients du nouveau. Tout oubli génère des soucis, ici les services ne tenaient plus principalement car on ne s’était pas rendu compte que les clients "de l’ancienne" utilisaient le logiciel avec réglages trop extensifs pour leurs besoin. C’est pas tant que "ça marchait" que "comme ça faisait trop, ça faisait au moins ce qui était désiré".
  • Découpler génère beaucoup d’oportunité techniques. Une fois que tous les bugs initiaux ont été corrigés, on a pu faire évoluer le logiciel et apporter des choses qui étaient devenus une gageur d’amener dans l’ancien mode "monolithique".
  • Découpler permet de varier les technologies mais fait aussi parfois tomber les gens dans la recherche de "silver bullet". Une fois le SaaS passé en microservice, on a vu fleurir plein de nouvelles choses architecturalement (du FluxDb pour les stats, du elastic pour la recherche dans les logs, du restful de partout pour communiquer…) mais du coup on est passé de "une application java" à "faut tout migrer en go car java c’est lourd, c’est lent, et puis même que c’est lourd" (c’est presque verbatim).

Projet 2 : Dans une asso pas très connu d’ici on a réécrit le module d’interprétation du markdown

Je vous fais pas un dessin, on parle de zmarkdown, sur zds.

Initialement on a eu des soucis avec la traduction markdown -> latex.

Ensuite on a eu des soucis avec les versions plus récentes du pythonMarkdown pour y mettre nos extensions.

Et puis en plus côté python le code était très performant pour faire du html mais impossible d’extraire quoi que ce soit (ping par exemple).

Et d’autres broutilles.

Alors on a réécrit le moteur complet. A l’époque le choix s’est porté sur la bibliothèque JS remark pour faire le taf. Ce fut long (6 mois), mais in fine on avait :

  • plus de fonctionnalités (ping, blocs neutres…)
  • plus de prévisibilité
  • des PDF qui était beaux (comparés à avant)
  • des ebook ouvrables sur calibre and co

Le projet était long et à cause de ça, au moment de sa sortie on avait déjà du code legacy. Après c’est lié à la gouvernance du projet zds.

Aujourd’hui, on a quelques soucis avec la bibliothèque remark car son auteur a fait une réécriture de zéro qui casse les API (mais la lib est objectivement meilleure maintenant), problème réécrire nos extensions est pire que chronophage.

En gros je tirerais quelques conclusions:

  • même pas trop mal géré en terme de gouvernance et de refacto fréquent un logiciel peut finir par être bloqué par ses choix techniques initiaux, ce blocage peut mener à une réécriture.
  • La gouvernance du projet est le point important, tant dans la manière dont c’est développé (ici quatre personnes dans leur coin jusqu’à ce que ça sorte) mais aussi dans le modus operandi. Une entreprise aurait par exemple pu imaginer sortir un autre produit avec les fonctionnalités manquante et faire du package commercial. En somme la communication entre les décideurs (pas ceux de Bigard, ceux qui gouvernent le projet) et ceux qui développent est primordiale et elle doit être claire pour gérer les attentes de chacun. Par exemple, on n’a pas fait de passage à zmd pour les perfs même si in fine le runtime de node permet des gains de perfs.

Projet 3 : Goodbye legacy, good morning WebPlatform

Il s’agit du projet sur lequel je travaille actuellement, dans l’entreprise qui m’emploie actuellement.

En gros, historiquement l’entreprise a un progiciel construit avec JEE (des JSP pour builder le front) et surtout pas mal de débutants qui font leurs premières armes sur le projet.

In fine, le produit s’est bien vendu, et des fonctionnalités avancées ont vu le jour, mais la maintenance devenait impossible. Le concept de refactoring ou de clean as you code n’était pas une chose connue de l’entreprise à ce moment.

Image de marque, fuite des développeurs, time to market qui s’allonge gravement, l’entrerpise a profité d’une période faste en terme financier pour planifier le passage à un nouveau projet (toujours en JEE, mais avec JSF cette fois-ci) qu’elle nommera WebPlatform.

Cela ressemble pas mal à ce qu’on a connu avec Wayland sauf que l’entreprise a communiqué rapidement à ses clients :

  • Le produit est commercialement séparé en module, voici l’ordre des migrations
  • Quand on commence la migration d’un module, il passe en "debug only" sur legacy, les autres continuent à recevoir des features
  • Quand la migration des fonctionnalités d’un module est 100% opérée, tout en ajoutant au passage quelques fonctionnalités qui étaient bloquées avant, on vous aide gratuitement à migrer.
  • Quand le dernier module sera totalement terminé, seuls 6 mois de support seront fournis sur legacy.

Cette méthode a pris 6 ans.

Certains clients ont eu du mal à migrer "car legacy ça marche" (jusqu’à ce que ça ne marche plus, y’avait des raisons pour lesquelles l’entreprise a demandé de refaire).

Pendant ce temps, de vraies politiques de "refacto continuel" ont été mises en place. A titre indicatif, sur une durée de 3 mois, la direction de l’entreprise s’attend à ce que l’équipe de développement passe :

  • 40% de son temps sur des nouvelles features demandées par les clients ou par la cellule stratégie produit de l’entreprise
  • 20–30% de son temps à corriger les bugs
  • 30–40% de son temps à fournir de l’amélioration technique (UI/UX, refacto, changement d’algorithmes, amélioration des perfs là où ça vaut le coup…)

Et ça marche bien, le produit est stable, il évolue bien et gagne des fonctionnalités à un bon rythme. Par expérience cependant, dès qu’un bout de code a plus de 2 ans d’ancienneté, va falloir songer à le refacto car on fait mieux, avec de meilleurs algo, de meilleurs libs, de meilleurs pattern…

Ah et bien que ce fut difficile, on a réussi à mettre en place des tests unitaires, d’intégration et end-to-end ce qui était inexistant dans l’application legacy.

Côté conclusion, je dirais donc :

  • une "refonte de 0" peut avoir du sens si les méthodes de développement sont aussi refondues
  • quoi qu’il arrive, un "refacto au fil de l’eau" est plus rentable pour l’entreprise, et avoir une direction qui le comprend est un confort immense.
  • par contre ça n’élimine pas les "tunnels" de développement, à un moment, certains refacto demandent quand même à passer 3–4 jours juste pour que ça compile, et ensuite il faut tester et peaufiner.

Edit: Après avoir posté je me rends compte que c’est un sacré pavé, désolé d’avoir été si peu synthétique.

+8 -0

Pro*C

Ah… les mauvais souvenirs de mon premier job en présta…

Cela dit je plussoie ton argument, pour avoir passé du temps à comprendre les entrailles cette techno (afin de pouvoir améliorer l’expérience développeur de l’équipe) et étant donné la qualité du code du projet, je pense que c’est le meilleur choix qu’iels pouvaient faire à l’époque (le projet à 25+ ans, un peu comme moi).

+0 -0

@artagis Pour limiter les problèmes d’adhérence aux libs, il me semble essentiel d’adopter le design pattern interprète (de l’architecture hexagonale si tu veux) pour délimiter proprement l’adhérence aux libs externes et pouvoir facilement réécrire l’adhérence aux libs externes en cas d’évolution. Ça ne coûte pas beaucoup plus cher à faire en plus.

Alors

  • ça ressemble à une réponse GPT
  • ça coûte "pas plus cher" quand tu démarres un projet de 0 et que ton projet a une fin
  • en vrai, si, ça coûte plus cher, surtout quand certaines libs te sont imposées et ne peuvent pas bouger, notre entreprise se branche à des systèmes BI dont SAP BOBJ. SAP BOBJ fournit un SDK Java. Il n’existera jamais de méthode alternative à ce SDK. Même avec une architecture en couche (archi initiale) ou hexagonale, tu n’auras JAMAIS d’autres possibilité. Et tu auras TOUJOURS les limites imposées par ce SDK.
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte