Ces derniers mois, nous avons procédé dans mon entreprise à un gros chantier de réécriture de code. Ce chantier a été lancé durant l’été, et s’est déroulé de septembre à décembre, avec une mise en production à Noël. J’ai voulu en parler sur le blog pendant qu’on bossait dessus, mais je me suis révisé, préférant attendre la fin de ce travail pour pouvoir faire un vrai retour sur expérience.
Les raisons initiales
Pour commencer, je voudrais vous parler du «combat interne» qui s’est livré dans mon crâne. Parce que je garde toujours à l’esprit une règle importante : Améliorer par évolutions et pas par révolution
Cette règle est un garde-fou salvateur. Quand on rencontre des difficultés pour faire évoluer un développement, il est souvent très tentant de tout effacer et de tout recommencer. Mais si cela est envisageable pour un projet personnel, c’est plus difficile à concilier avec les nécessités économiques d’une entreprise. Néanmoins, nous sommes quand même repartis d’une feuille quasi blanche. Comment est-ce arrivé ?
Plusieurs événements se sont additionnés les uns aux autres, sur un intervalle de temps d’à peine quelques semaines :
- Nous avons rencontré des problèmes de performance sur les sites que nous éditons. Plusieurs passes d’optimisation avaient été nécessaires pour atteindre un niveau satisfaisant, mais cela restait inférieur à ce que j’estimais “normal”. Ces soucis provenaient pour la plupart de requêtes SQL mal fichues ; mais après avoir les avoir optimisées, nous nous sommes rendu compte que sur certains aspects nous avions atteint les limites de notre modèle.
- Nous avions alors du code un peu ancien, qui avait vécu plusieurs évolutions, et qui avait survécu à plusieurs changements dans les choix fonctionnels. Malgré plusieurs refactorisations, le code contenait toujours des scories qui nuisaient à ses performances, mais surtout qui le rendaient de plus en plus difficile à maintenir et à faire évoluer.
- De nouvelles fonctionnalités devaient être développées, certaines imposant des modifications du modèle de données, et non pas seulement des ajouts. À cela s’ajoutait une refonte graphique des sites, qui impliquait de toute manière de réécrire les «points de contact» entre le code pur et les templates de pages.
Chacune de ses raisons nous poussaient à envisager une réécriture partielle de notre code.
J’ai lu un jour un article qui parlait des spécificités des développements open-source. Il contenait un conseil assez intéressant : Si vous voulez écrire un logiciel de bonne qualité − hors de toute notion de rentabilité − il vous suffit de l’écrire, puis de jeter le code et de tout réécrire. Le fait d’avoir été confronté aux problèmes une première fois fait que vous saurez la direction à prendre pour votre développement, en évitant les errements par lesquels vous serez passé la première fois.
Les raisons supplémentaires
Pendant la phase d’analyse technique, pendant laquelle nous réfléchissions aux différents moyens nous permettant d’atteindre nos objectifs, nous avons fini par converger vers une solution technique hybride. Nous avons choisi de mélanger l’utilisation d’une base de données relationnelle (MySQL) et d’une base de données non relationnelle (FineDB, développée en interne). Le but est de concilier le meilleur des 2 solutions :
- On a souvent besoin de récupérer des bloc de données agrégées. Par exemple, un article avec ses commentaires, ou une question avec ses réponses, forment un tout qui peut être chargé d’un seul morceau pour être affiché sur une page. Pour cet usage, les bases de données non relationnelles orientées document sont idéales, car elles permettent d’accéder très rapidement à un «document».
- Par contre, on a toujours besoin d’accéder à des informations composites. Par exemple, pour afficher une liste de questions/réponses par ordre chronologique, il est hors de question d’ouvrir tous les documents, ce serait bien trop lent. D’autant que nous devons faire des requêtes assez complexes, en utilisant un système de catégorisation hiérarchique de nos contenus. Un base de données relationnelle classique excelle pour ce genre d’usage.
- Nous avons donc décider de séparer d’un côté les données (entre autre, tous les textes), stockées sous forme de documents, et les méta-données (identifiants, dates, …), stockées sous forme relationnelle. Notre base de données MySQL est ainsi passée de plusieurs giga-octets à une centaine de méga-octets. Le gain en performance est évident. En plus, les longs textes ayant été retirés, toutes les tables sont maintenant de taille fixe, ce qui ajoute un gain de 20% à 30% (d’après Michael Widenius, dit Monty, le créateur de MySQL).
En plus de ça, nous voulions changer la manière dont nous gérions nos médias (photos, vidéos). Jusque-là, ils étaient stockés sur un serveur spécifique, mais nous voulions le rapatrier en local sur nos serveurs frontaux pour réduire les temps de latence. C’est pour cela que nous développions FineFS, un système de fichier réseau, depuis un an.
Comment ça s’est passé
Au mois d’août dernier, nous avions fait les expérimentations qui nous confortaient dans le choix hybride décrit ci-dessus. Et je suis parti en vacances pendant 2 semaines. Je ne sais pas si vous connaissez Annecy, mais c’est une très belle ville. Bref, assis dans l’herbe du parc d’Annecy, face au lac, avec mon ordinateur portable sur les genoux, seul avec ma femme et ma fille, j’étais hyper-productif. J’ai développé la quasi-totalité de notre couche d’accès aux données (en utilisant le couple MySQL-FineDB), j’ai fait des améliorations profondes dans notre framework (développé en interne, j’ai en tête de le publier sous licence libre, on verra), réécrit les contrôleurs pour les adapter aux changements dans le framework, revu la gestion des médias… En 3 ou 4 heures par jours, j’abattais bien plus de boulot que pendant une journée de travail classique.
J’étais donc plein d’entrain à mon retour de vacances, persuadé que nous allions pouvoir mener à bien cette réécriture complète en peu de temps. Le débat «évolution vs. révolution» me semblait clos de lui-même. Travailler par évolutions successives aurait été mieux, mais la refonte était à ce point profonde qu’il était impossible de la découper en plusieurs phases.
Début septembre, j’ai accueilli deux nouvelles personnes dans mon équipe. Un administrateur système, pour remplacé celui qui était là depuis 3 ans et qui allait partir rejoindre sa belle à l’étranger. Et un développeur.
Si la formation du nouvel administrateur a été majoritairement faite par l’ancien, je me suis chargé d’enseigner au développeur nos méthodes, nos outils, les bonnes pratiques de développement que nous utilisons. Et sa montée en puissance a été plus longue que je ne le pensais. J’ai beau savoir pertinemment que c’est un long processus, j’avais recruté un développeur expérimenté avec l’espoir que cette période pourrait être réduite. Pendant un bon mois et demi, ce développeur n’a pas été pleinement productif.
Le développement a donc duré plus longtemps que prévu. Durant ce temps, deux facteurs ont encore empiré les choses :
- La refonte graphique a pris du temps à se faire. Nous avons passé plus de temps que prévu sur les maquettes, pour trouver la bonne ergonomie. Ce qui a décalé l’intégration HTML/CSS.
- Malgré le postulat initial, qui était de réécrire le code pour repartir sur des bases techniques saines, mais en restant sur le même périmètre fonctionnel, il y a eu des demandes d’évolutions qui se sont ajoutées aux développement en cours. Ce qui n’était qu’un refactoring s’est ainsi transformé en refactoring+développement, avec tout ce que ça implique comme délai pour procéder à des analyse et écrire les spécifications techniques.
- Pire, alors que toute l’énergie de l’équipe technique aurait dû être focalisée sur l’accomplissement de ce refactoring, nous avons été obligés de faire de la maintenance sur l’application existante. La correction de bugs est normale ; si on trouve un bug critique, il est logique de le corriger au plus tôt, sans attendre que la nouvelle version soit prête. Par contre, l’ajout de fonctionnalités n’était pas prévu, a été demandé, et a fini par être accepté.
La mise en production a été effectuée juste avant Noël. Avoir une deadline est motivant pour l’équipe. Et s’il fallait se prévoir une semaine avec peu de trafic sur nos sites, pour résoudre les éventuels problèmes, celle entre Noël et le jour de l’an était parfaite.
Nous avons tout de même mis en place un travail itératif : Certaines fonctionnalités ne pouvaient pas être prêtes à temps, on a fait ce qu’il fallait pour préparer une release fonctionnelle, puis avons ajouté les fonctionnalités manquantes au fil des versions, pour aboutir à un produit complet début février. Pour la peine, le produit est vraiment complet, avec un grand nombre de nouvelles fonctionnalités. C’est bien simple, nous avons intégré quasiment toutes les nouvelles fonctionnalités qui étaient prévues au planning pour la fin 2010, à l’époque où le refactoring n’était pas encore à l’ordre du jour !
Conclusion
Je retire de tout cela plusieurs choses, certaines qui semblent évidente a posteriori et d’autres qui sont nouvelles pour moi :
- Quand je développe en vacances, sans la moindre interruption, je suis bien plus efficace qu’au bureau. Je ne dois donc pas croire que je pourrais tenir le même rythme dans la durée. Parce que même si j’ai une équipe de développeurs, je reste − par mon expérience plus grande (principalement) et par mon statut de directeur technique (un peu) − la locomotive quand il s’agit de faire des choix importants et de les mettre en place.
- Même lorsque tout le monde est d’accord pour se concentrer sur la nouvelle version d’un projet, se tape dans la main en se disant qu’on ne touche plus à la version en cours d’utilisation, cela reste un doux rêve. Il y a toujours des évolutions ou des débuggages qui seront à faire sans attendre. Il faut s’y préparer.
- Si on vous dit que les spécifications fonctionnelles sont terminées, ne le croyez pas. Il y a de fortes chances pour que les équipes fonctionnelles soient parties directement de l’expression de besoin du client (qu’il soit interne ou externe), et n’aient pas consacré suffisamment de temps à décrire tous les cas possible ; entre autre, les cas d’erreur sont souvent négligés. Prenez le temps de passer en revue les spécifications fonctionnelles (avec ceux qui les ont écrites), pour rédiger les spécification techniques.
- Écrivez les spécifications techniques avec tout le soin nécessaire. Elles constitueront votre socle, la référence vers laquelle vous vous tournerez lorsque vous vous poserez des questions d’implémentation. Accessoirement, elles risquent d’être le seul élément constant dans un univers où les spécifications fonctionnelles changent parfois plusieurs fois par jour.
- Il faut trouver le juste équilibre entre la souplesse d’un côté, qui vous fera accepter des modifications fonctionnelles en cours de route, et la fermeté de l’autre côté, qui vous évitera de sortir votre projet avec «trop» de retard.
L’une de nos erreurs (dont je suis à l’origine) fut de partir convaincu qu’il ne s’agirait que d’un refactoring isofonctionnel. Lorsque les premières demandes d’évolutions sont arrivées, nous avons pu les garder en dehors du périmètre. Mais au fur et à mesure que le projet s’étirait en longueur, la pression montait tellement que nous nous sommes retrouvés à accepter des demandes sans pour autant en garder une trace formelle, comme si ces évolutions avaient toujours fait partie du périmètre fonctionnel.
Si vous vous retrouvez dans une situation où vous négociez l’ajout d’une fonctionnalité au sein d’un développement en cours, c’est que vous êtes sur la pente glissante qui va vous amener à vous casser la gueule méchamment. Vous vous souviendrez des ajouts qui vous auront été imposés, mais les “fonctionnels” que vous avez en face de vous ne se souviendront que de 2 choses : la date de mise en production initialement prévue, et le fait que vous soyez en retard.
J’exagère intentionnellement. Au final, l’expérience ne fut pas si négative. Mais ce qui est certain, c’est que nous sommes heureux d’en avoir terminé et de revenir à des projets plus classiques, moins «globaux».