Nombreux sont les défis rencontrés lors de la reprise d’un projet qui est en phase avancée de développement, voire en production depuis quelque temps. Et même si la refactorisation devrait faire partie d’une boucle normale de développement, il n’est pas toujours évident de considérer ce temps comme un investissement.
J’ai eu l’occasion de travailler ces derniers mois sur deux applications souffrant de lourdes dettes techniques, et pour l’une d’elles, il a fallu mettre en place une stratégie de paiement progressif, plus aisée à appliquer pour la plupart des projets de développement.
À première vue, évaluer le coût d’une dette technique est très complexe. Il faut également se rappeler que si on n’y consacre qu’un minimum d’efforts, on finira toujours par ne payer que les intérêts. Chaque nouvelle fonctionnalité ne sera alors qu’un nouveau prêt, s’ajoutant dans le meilleur des cas au principal. Afin de rattraper le retard et de poursuivre en payant ses mensualités à temps, il faut évaluer la dette actuelle et revoir certaines définitions et pratiques au sein de l’équipe.
Est-ce que des librairies pas à jour ou l’utilisation d’une API obsolète constituent une dette technique ? Qu’en est-il des bogues connus ? Et des problèmes de performance qui découlent de ces précédents points ? Et de chaque raccourci pris dans le passé simplement pour pouvoir livrer à temps sans réellement régler le problème ? Oui, ils sont tous constitutifs de la dette globale. Est-ce qu’une architecture non optimale fait partie de cette dette technique ? Probablement non, et le changement d’architecture serait un thème à traiter à part. Et le déficit en tests unitaires ? Ce n’est pas non plus une dette en soi, mais l’implémentation de ceux-ci fera sans doute partie des bonnes habitudes qui permettront de ne pas faire croître la dette à nouveau.
La première étape inévitable est de disséquer l’application et de regrouper par thème chaque élément de la dette. Le temps nécessaire à cette analyse dépendra directement de l’implication antérieure de l’équipe dans le projet. Une fois une vue d’ensemble acquise, les différentes parties de la dette peuvent être regroupées en trois catégories principales :
- celles qui sont d’ordre général, qui peuvent être payées sans conséquences directes pour les fonctionnalités, et qui sont donc chacune candidates à leur propre récit (par exemple : “mettre à jour React à la dernière version”) ;
- celles qui sont d’ordre général, mais qui devront être répétées pour de multiples fonctionnalités, et qui sont donc candidates à devenir des sous-tâches (par exemple, effectuer la transition de classes à des composantes fonctionnelles) ;
- celles qui sont spécifiques à un récit et qui ne seront pas dupliquées (typiquement, un bogue concret).
Avant de commencer l’évaluation des efforts à attribuer à chaque élément de la dette, il est important de redéfinir la notion d’achèvement. À quel moment considère-t-on un récit comme “fini”, que la “story” peut être fermée ? Avant que les tests d’assurance qualité ne débutent ? Après les tests d’AQ et le “merge” dans la branche de développement principale, mais avant les tests de contrôle qualité (CQ) ? Ou encore, seulement au moment où la fonctionnalité est livrée aux utilisateurs ?
Dans le cas de mon équipe, l’approche qui s’est avérée la plus bénéfique fut de considérer un récit comme “fini” une fois que les tests d’AQ étaient réussis et que notre code se trouvait dans notre branche de développement. Toutefois, notre entente stipulait que cette branche devrait être livrable à tout moment. En conséquence, aucun “merge” incomplet ou problématique n’était en aucun cas permis.
Lors du premier “grooming”, nous avons dû mettre en place une méthodologie permettant une réduction systématique de la dette. Cette méthodologie se révéla en fin de compte assez simple, tout en étant méticuleuse et très efficace.
Il a été convenu qu’un maximum de 20 % du sprint serait consacré au paiement de la partie de la dette d’ordre général (la première catégorie de notre liste plus haut), celle qui ne nous créerait pas de bogues affectant les fonctionnalités individuelles.
Pour la deuxième catégorie de dette, c’est-à-dire les parties qui sont globales, mais qui doivent être répétées, il a été décidé d’y aller progressivement. Chaque fois que l’on toucherait à une fonctionnalité existante, deux sous-tâches seraient créées automatiquement pour le récit, l’une pour la refactorisation du code et l’autre pour les tests unitaires. Ces deux sous-tâches représentaient dans la majorité des cas de 15 à 30 % des points du récit (“Story Points”).
La troisième catégorie de la dette, qui porte sur des bogues spécifiques à une fonctionnalité, serait payée selon les priorités établies dans le carnet de produit (“Backlog”).
Au bout de trois sprints avec cette approche, le résultat fut sans appel. Chaque refactorisation du code nous a permis d’éliminer beaucoup de raccourcis qui avaient été pris auparavant, d’améliorer la modularité et de tester chaque ligne de code, ce qui a eu un impact direct sur la facilité de maintenance et d’implémentation, la fiabilité et les performances. Les bénéfices étaient palpables des deux côtés de l’application : pour les développeurs et, tout aussi important, pour les utilisateurs. Au final, la taille du paquet de l’application a été réduite de 70 % (de 12 à 3,5 Mo), le temps de chargement de 80 % et la couverture en tests unitaires est passée de 2 à 60 % (ce qui représente près de 380 nouveaux tests).
Même si au départ tout ce travail semblait représenter un effort relativement flou et un coût supplémentaire inconnu, il s’est avéré finalement que les bénéfices obtenus justifiaient largement l’investissement, et ce sur long terme. Il n’est parfois pas aisé d’être complètement transparent sur la dette existante, et d’exposer celle-ci à toute l’équipe par le biais du carnet de produit, mais en mettant en place des métriques pour chaque objectif, l’avancée est plus que gratifiante pour toutes les personnes impliquées.