Le Refactoring

Le refactoring est une démarche de programmation popularisée par le processus Extreme Programming, dans laquelle les développeurs améliorent en permanence la structure de leur code. Cet article propose une vision d'ensemble des motivations et des enjeux de cette approche.

L’effet spaghetti

Quel développeur n’a pas déjà travaillé dans du “code spaghetti” ? Mieux encore, combien aujourd’hui ne travaillent pas dans un code mal conçu, fragile, et difficile à faire évoluer ?

Vous connaissez certainement le phénomène. Dans ses premières heures, le projet baigne dans une certaine euphorie. On découvre de nouvelles technologies, on conçoit la structure de la future application. Mais au fil des mois, ce travail de conception fait place à la guérilla des “petits détails d’implémentation” qui ont tôt fait de mettre à mal la structure initiale. Faute de temps, les développeurs prennent des raccourcis. Ils bricolent des verrues qu’ils corrigeront “plus tard”, lorsqu’il y aura moins de pression. Le code devient plus difficile à appréhender, la peur d’y introduire des régressions accélère l’accumulation des verrues. La livraison approche, ce n’est plus le moment de remettre en question la conception initiale !

Après quelques années - parfois quelques mois - d’un tel traitement, le code est devenu un amas de cas particuliers dans lequel les évolutions se révèlent très difficiles à implémenter. Selon les cas, ces évolutions se poursuivront à un coût très élevé, ou bien l’organisation, prétextant un changement de technologie, entamera une réécriture complète.

Quel est le coût de cette façon de procéder ? La lourdeur, la dégénérescence progressive du code sont-ils des maux inéluctables ?

La promesse agile

Pour une équipe qui a vécu ce type de situation, l’approche agile est séduisante. Livrer fréquemment, être capable de réagir facilement à des changements de priorités business... Tout cela est bien beau, mais si l’équipe conserve ses anciennes habitudes de programmation, elle sera très rapidement rattrapée par la réalité de son code et se trouvera incapable de tenir ses promesses.

Le code de l’application n’est pas le seul concerné. Parmi les démarches agiles, l’Extreme Programming a popularisé la pratique d’automatisation intégrale des tests. Là encore, l’expérience à montré que ces fameux tests automatiques, s’ils ne sont pas correctement construits et maintenus, peuvent à leur tour devenir très difficiles à maintenir et ralentir très fortement la progression de l’équipe.

Avec l’expérience, l’équipe de développement agile prendra certainement conscience du fait que sa vitesse de développement est conditionnée par la qualité de son code et de ses tests. Pour elle, la question se posera alors en ces termes : comment s’assurer que le code reste aussi propre et facile à modifier que possible, même après plusieurs mois ou plusieurs années de modifications successives ?

La solution XP : le refactoring

En complément de l’automatisation des tests, l’Extreme Programming a popularisé une démarche de programmation appelée refactoring, que l’on traduit parfois en Français par “remaniement”. Le principe en est simple : il s’agit d’améliorer la structure du code sans changer son comportement.

L’exemple de refactoring le plus simple que l’on puisse trouver est peut-être la suppression de code mort. Quand je retire une fonction qui n’est pas utilisée, je change le code sans changer le comportement du programme. Dans le même ordre d’idées, quand je modifie le nom d’une classe, d’une fonction ou d’une variable, je change là aussi le code sans changer le comportement du programme.

Dans son livre Refactoring, Martin Fowler détaille une liste de plus de 70 remaniements : extraire un bloc de code pour en faire une fonction, déplacer une méthode vers une classe mère, ajouter un paramètre à une fonction, changer l’ordre des paramètres d’une fonction, etc. Toutes ces techniques de modification se combinent pour créer une sorte de langage de modifications du code. Avec le temps, et avec l’utilisation intensive des nombreux refactorings automatiques intégrés aux environnements de développement modernes, le développeur XP finit par modifier en permanence son code, rappelant la façon dont on peut modeler et remodeler une pièce d’argile.

Once and Only Once

Modifier le code n’est pas une fin en soi. Les modifications doivent conduire à une amélioration significative de la simplicité et de la flexibilité du code, et l’application d’une suite aléatoire de remaniements sur un morceau de code aura peu de chances de produire un tel résultat.

Le remaniement n’est donc pas une fin, mais un moyen pour obtenir un code propre. La question devient alors : qu’est-ce qu’un code propre ? Quelles sont les propriétés qui rendent un code facile à appréhender et à faire évoluer ?

Les concepteurs d’XP avancent une réponse : un code facile à faire évoluer, c’est un code dans lequel, pour une évolution donnée, je n’aurai qu’une seule modification à effectuer. En d’autres termes, il s’agit d’un code dans lequel chaque concept n’est exprimé qu’une seule fois. Dans cette logique, l’un des objectifs principaux du remaniement devient l’élimination de toute forme de duplication dans le code.

Cette conception pilotée par l’élimination de la duplication n’est pas une spécificité XP. Dans son livre Multi-Paradigm C++ Design, Jim Coplien, l’une des figures de proue du mouvement qui a donné naissance aux Design Patterns, proposait en 1998 une démarche de conception basée sur la factorisation du code à partir de l’identification des éléments communs et variables. Le livre explique en particulier comment la plupart des mécanismes des langages de programmation peuvent être vus comme des mécanismes de suppression de la duplication.

Par exemple :

  • Les constantes servent à éviter la duplication d’une valeur codée en dur, permettant de ne conserver qu’un seul point de modification de cette valeur dans le code.
  • Les fonctions servent à mettre en commun des blocs de code qui ne diffèrent que de quelques paramètres. Il suffit de changer le contenu d’une fonction pour modifier le comportement de toutes les portions de code qui l’utilisent.
  • Les templates (ou generics) permettent d’éliminer la duplication de fonctions ou de classes qui ne diffèrent que par l’utilisation de certains types de données.
  • A un niveau de granularité plus large, les librairies et les frameworks sont aussi des façons d’éliminer de la duplication, mais cette fois entre plusieurs applications.

Au départ, un développeur qui découvre le remaniement reste souvent focalisé sur des modifications locales du code. Avec l’expérience, il parviendra à entamer des changements plus larges, qui pourront influencer la conception d’ensemble de l’application.

Conception incrémentale de l’application

Contrairement à une idée largement répandue, le remaniement n’est pas une activité épisodique. Dans une équipe XP, le remaniement est complètement intégré à l’activité quotidienne de programmation - il est même indissociable de la programmation. Des tâches de remaniement pur s’imposent parfois, mais ce n’est en aucun cas la norme.

Cette utilisation systématique du remaniement amène en pratique à une approche radicalement différente de la conception. Dans un projet XP, la structure de l’application n’est pas conçue au début du projet, mais émerge véritablement de cette activité d’amélioration continue du code.

J’ai pu observer cette émergence sur un projet XP démarré en 2002, dans le cadre d’un grand projet télécom. Lancé au départ avec une équipe de trois développeurs, il occupait en 2005 plus d’une vingtaine de développeurs, avec une application qui représentait quelques centaines de milliers de lignes de code, des milliers de classes, et environ 20.000 tests. En 2003, nous avons extrait une partie “plateforme” gérée par une équipe dédiée de huit développeurs, avec une équipe d’une quinzaine de développeurs dédiée à l’écriture de plugins pour l’adaptation à une technologie réseau spécifique. Plus tard, des équipes américaines, russes, puis chinoises ont démarré des projets basés sur cette plateforme, pour l’adapter à d’autres technologies réseau. Aujourd’hui encore, la plateforme continue d’évoluer, et le nombre d’équipes utilisatrices augmente. Après six ans d’évolution, le code de l’application est toujours jugé propre.

L’élément intéressant de cette expérience est que la phase de conception initiale de l’application, en termes de conception interne, de modules, de classes, n’a duré qu’environ une demi-heure. J’ai écrit les premières lignes de code pour atteindre un premier objectif très simple, alors que j’étais encore seul sur le projet. Après quelques semaines, nous étions trois à enrichir progressivement la structure. La notion de plateforme générique n’est apparue qu’après environ un an de développement. Les concepts qui sont aujourd’hui au coeur de la structure de cette application ne sont apparus qu’après quelques mois ou quelques années d’évolution.

Au fil du temps, l’équipe a ainsi pu faire émerger la conception de cette application. Des blocs de code sont devenus des fonctions, des fonctions se sont agrégées en classes, des classes se sont agrégées en modules, des interfaces sont apparues pour découpler des modules, certaines portions du code sont devenues suffisamment déclaratives pour être “poussées” dans des fichiers de configuration, etc.

Il ne faut toutefois pas imaginer que cette émergence est survenue par magie. Au quotidien, l’équipe passait du temps à travailler sur la conception à travers cette activité de remaniement. Les développeurs étaient également formés aux techniques de conception, et maîtrisaient les Design Patterns et les principes avancés de conception objet tels que le principe d’ouverture/fermeture, le principe de substitution de Liskov, etc.

Au-delà de la suppression des duplications, ils étaient capables de faire apparaître des modules et de gérer les dépendances entre ces modules.

L’émergence évoquée ici n’est donc pas un phénomène hasardeux, mais plutôt la concrétisation d’une vision que les développeurs affinent et corrigent au fur et à mesure des développements.

Remaniement des tests

Modifier un code existant, cela introduit des risques de régression - c’est d’ailleurs pour cette raison que de nombreuses équipes n’osent pas changer le code de leur application. Dans le cas d’un projet XP, en revanche, la donne change : les tests automatisés sont justement là pour garantir que toutes les régressions seront interceptées rapidement.

Les tests automatiques sont ainsi sans nul doute le complément indispensable du remaniement. Mais il se trouve que l’inverse est également vrai : le remaniement est le complément indispensable des tests automatiques.

Dans notre application, le code de test représentait 56% du volume total de code. Si l’on part du principe que la qualité du code conditionne la vitesse de développement de l’équipe, on en conclut que l’équipe avait intérêt à focaliser ses efforts de conception sur le code de test !

Nous avons appris cette leçon “à la dure”, après nous être retrouvés dans une situation où une évolution a demandé une heure de travail dans le code, plus quelques jours de travail pour la mise à jour de plusieurs centaines de tests. Nous avons également été confrontés au problème des tests devenus trop difficiles à écrire parce que les API de test que nous avions développées n’étaient pas assez évoluées.

Nous avons tiré deux conclusions de ces expériences :

  • Le code des tests doit lui aussi être remanié en permanence. Nous avons d’ailleurs observé le même phénomène d’émergence : les tests dont nous disposions après quelques années étaient bien plus concis et lisibles que ceux que nous avions écrits en début de projet.
  • Il faut trouver le bon équilibre entre tests unitaires et tests de recette. En particulier, les tests unitaires introduisent le risque de figer le code en épousant de trop près la conception existante. Les tests de recette, en se positionnant en test « boîte noire », permettent de capturer les exigences finales en dépendant moins des détails d’implémentation. Les deux catégories de test présentent leurs intérêts, il faut savoir comment les combiner efficacement.

Conclusion

La dégénérescence progressive du code est un mal qui guette tous les projets, agiles ou non. L’une des contributions majeures d’Extreme Programming a été d’apporter une réponse concrète à ce problème, à travers deux pratiques complémentaires : les tests automatisés d’une part, et le remaniement, une activité d’amélioration permanente du code, de l’autre.

J’ai évoqué ici la façon dont cette approche pouvait conduire à l’émergence d’une structure très propre pour une application, donnant lieu à une conception incrémentale du logiciel qui permet de protéger l’investissement consenti pour son développement.

Cette approche pourra séduire certaines équipes qui ne sont toutefois pas disposées à mettre en place l’ensemble des pratiques d’Extreme Programming. Mais est-ce possible ? Peut-on utiliser le remaniement en dehors d’XP ?

D’abord, il faut savoir quel niveau de performance on recherche. Quel que soit le contexte projet, une approche d’amélioration permanente du code sera certainement positive et retardera sa dégénérescence. En revanche, si l’on vise l’émergence progressive d’une conception très propre, un certain nombre de facteurs doivent être réunis :

  • L’équipe de développement doit avoir une très bonne maîtrise des techniques de conception (utilisation des interfaces, gestion des dépendances, etc.).
  • L’équipe doit se construire une vision partagée du logiciel, qui est indispensable pour garantir l’intégrité d’ensemble de la conception. Pour cela, les développeurs doivent être capables d’aborder des sujets de conception sans que les discussions ne se tranforment en guerres de tranchée.
  • L’équipe doit travailler dans un environnement projet qui ne la pousse pas systématiquement à prendre des raccourcis sur la qualité du code.

L'Extreme Programming n’est pas la seule façon d’obtenir ces résultats, mais pour bien des équipes aujourd’hui, il s’agit sans nul doute d’un très bon point de départ.