À propos du refactoring

Je vous ai déjà parlé de refactorisation. Voici un retour d’expérience un peu atypique.

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 ces raisons nous poussait à 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 :

  1. 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.
  2. 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.
  3. 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.
  4. É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.
  5. 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».

15 commentaires pour “À propos du refactoring

  1. Merci pour ce retour d’expérience, on a toujours quelque chose à y apprendre. Je reste cependant très mitigé sur le dernier paragraphe…

    Les méthodes agiles ont du bon et permettent justement de coller au plus proche du besoin, qui de toutes façon évolue tout au long du projet.

    Il me semble qu’il est plus important de s’approcher au plus près du besoin que de fournir un résultat à tout pris pour la date donnée.

  2. Oui et non.

    Ce que je dis dans le dernier paragraphe est valable pour le contexte très particulier dont je parle dans l’article : un refactoring profond qui a duré plusieurs mois, et qu’il était important de mener à bien assez rapidement pour ne pas hypothéquer les évolutions futures. Ajouter des fonctionnalités supplémentaires alors que le refactoring n’était pas terminé, c’était se mettre dans un tunnnel dont on ne voyait plus le bout.

    Il ne faut pas non plus oublier que les méthodes agiles sont basées sur une approche itérative. Il vaut mieux sortir à temps un produit qui propose moins de fonctionnalités qui marchent, plutôt que d’attendre plus longtemps ou de sortir plus de fonctionnalités à moitié terminées.
    Plus tôt le service est en ligne, plus tôt il est confronté au marché et aux utilisateurs, plus tôt on gagnera en expérience pour l’améliorer par la suite.

  3. Pour continuer un peu dans la même ligné, je suis tombé sur un nouveau concept la semaine dernière:
    <a title=“Continuous Deployment” rel=“nofollow”>
    cad: on délivre en continue les évolutions par très petites touches et avec le minimum de test, en cas de bug la correction étant fait en live (pas retour à la version antérieure).

    ça me parait le prolongement des méthodes agiles et pour du refactoring le plus intéressant, on corrige/améliore le code par très petites touches.
    Un avis ?

    @Amaury: refaire le code complet est obligatoire au bout d’un certain temps quand même, là on est d’accord, ce n’est que pour changer de techno
    J’ai une autre question aussi:
    Est-ce que une architecture MVC (Modèle, Vue, Contrôleur) permet de se passer d’une refonte complète (permettant de le faire en plusieurs fois) ou non ?

  4. @Sylvain : Concernant ton premier point, c’est théoriquement envisageable. Sauf que :
    1. Les mises en production sont souvent des opérations un peu lourdes, qu’on évite de faire trop souvent.
    2. Le risque de mettre de la merde en production peut être trop grand.
    En fait, il faut trouver le bon équilibre entre développement trop itératif et le «surtest» qui immobilise.

    Concernant ton second point, ça dépend là encore de plusieurs choses. Dans notre cas, le framework était justement remanié ! Et comme notre architecture technique changeait du tout au tout, il était impossible de garder la couche modèle inchangée ; donc les contrôleurs devaient être réécrits pour s’y adapter.
    Mais dans le cas général, je suis d’accord qu’une architecture MVC est censée faciliter la refactorisation. Concrètement, il y a souvent des dépendances qui font qu’une modification dans le modèle (ou dans un objet métier) impose de modifier un certain nombre de contrôleurs et/ou de vues/templates. Mais ça reste quand même plus propre qu’un développement non MVC.

  5. Bonjour, j’ai une petite question : quand on a une équipe de développeurs, et qu’on doit répartir le travail entre
    – développer la future version et
    – maintenir/améliorer la version actuelle
    selon-vous, comment doit-on choisir entre les approches suivantes :
    – désigner une poignée de développeurs en « task force » qui doit se concentrer sur la nouvelle version
    – ou alors impliquer tout le monde dans la nouvelle version, et créer de la confusion en travaillant sur plusieurs fronts à la fois

    Et je ne parle même pas des conflits entre les développeurs à tendance « conservateur » qui copient/collent l’ancienne version ; et ceux qui par la « fuite en avant », délaissent complètement la version actuelle … On va dire que si les spécifications fonctionnelles sont suffisamment claires ce genre de situation n’arrive pas

    à bientôt

  6. @Cedric : Il n’y a aucune règle absolue. Tout dépend des projets en question, de leurs aspects stratégiques pour l’entreprise, de leur coût (de maintenance pour l’un, de développement pour l’autre), de l’argent qu’ils génèrent et/ou peuvent générer, du temps nécessaire à chaque tâche, …

    En fait, il n’y a souvent pas le choix. À partir du moment où il y a un «existant», il faut vivre avec. Même lorsqu’on voudrait se concentrer sur le prochain développement, il faut maintenir les projets qui font vivre l’entreprise.
    Ce qu’il faut viser, par contre, c’est de ne faire que de la maintenance, et pas de nouveaux développements, sur les anciens projets. Sinon ils finissent par phagocyter toutes les ressources.

    Il est souvent difficile de séparer les développeurs en deux équipes (une pour l’ancien projet, une pour le nouveau). Parce qu’on a besoin des compétences et de l’expérience de ceux qui ont bossé sur le projet actuellement en production, pour faire aller dans la bonne direction technique sur les nouveaux projets.
    Il est par contre possible de segmenter le temps passé sur chaque projet. Soit en prévoyant un certain budget-temps (1 jour, 1 jour et demi par semaine pour traiter les bugs, par exemple), soit en prévoyant carrément des sprints de maintenance au milieu des sprints de développements (dans le cas de la méthode SCRUM).

  7. merci pour les pistes 😉 j’imagine bien qu’il y a des tonnes de cas de figure différents

  8. Pour répondre à Cedric, avec l’expérience de ce type de config, il faut diviser l’équipe en deux.
    Bien sur il peut y avoir des points d’adhérence ponctuelle, parce qu’on a besoin des compétences de quelqu’un qui est dans l’équipe A pour l’équipe B et vice-versa. Mais ces cas là doivent être segmenter avec budget-temps. Couper l’équipe en deux de manière officielle, peut poser des jalousies dans un premier temps, mais c’est fait une bonne fois pour toutes et ça évite d’y revenir à chaque attribution de tâche. Et puis couper l’équipe en deux cela permet de garantir que l’équipe sur le nouveau projet ne se fera pas phagocyter par la maintenance et le support de l’ancien produit, ce qui provoquerais un nouveau projet qui dure trop longtemps avec une deadline impossible à maintenir et une perte de motivation dans l’équipe.

  9. @Amaury (sur l’article)
    C’est toujours marrant quand on voit qu’on a pas perdu la main et qu’on est resté un bon technicien, n’est-ce pas ? 🙂
    Je suis sur que toi aussi, tu voudrais plus de développeur comme toi dans ton équipe 😉

  10. @Anon : Pourquoi «pas perdu la main» ? Je n’ai jamais arrêté de développer.
    Je ne suis pas prétentieux au point de vouloir que mes développeurs soient mes clones. Cf. les commentaires sur l’article Les erreurs de débutant.

  11. @Amaury
    Je ne voulais pas être prétentieux ou dire que tu étais prétentieux. Avec le titre de ton blog , je me sentais un peu dans la même situation que toi : de geek à manager. Pour être franc, j’étais beaucoup moins rigoureux quand j’étais simple développeur 😉
    Par contre pour moi, le développement est maintenant une tâche occasionnelle, à cause du temps pris par les autres activités de mon poste.
    Et quand j’y retouche, je suis heureux de ne pas avoir perdu la main 🙂

  12. @Anon : Je sais, je sais 🙂
    Oui, quand on commence à gérer des équipes, on est forcé à gagner en rigueur.
    Je suis très content de ma position actuelle. Étant créateur et directeur technique d’une entreprise (qui a démarré toute petite et qui compte maintenant 30 personnes), j’ai des rôles très divers et tous très intéressants. Développeur, architecte logiciel, administrateur, architecte IT (au sens « infrastructure »), chef de projet technique. Je participe aux choix fonctionnels et ergonomiques. Je fais même de la RH.

    La seule chose sur laquelle je sens que je me rouille un peu au fil du temps, c’est la programmation en C. Pour un ancien codeur kernel, ça fait chier… 😉

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Notifiez-moi des commentaires à venir via email. Vous pouvez aussi vous abonner sans commenter.