Mesurer la qualité d'un projet pour le désendetter
En ingénierie logicielle, tant qu'un projet se développe, la dette technique s'accumule inexorablement. Les sessions de refactoring sont là pour contrebalancer cette tendance et leur mise en place régulière garantit la maintenabilité du projet. Mais ce qui est délicat avec la dette technique, c'est qu'elle n'est pas vraiment mesurable, et comme l'iceberg, on n'en voit que la partie émergée. Résultat, le refactoring est souvent sous-estimé et mal maitrisé. Si on ajoute à cela une mauvaise couverture de test, refondre le code applicatif fait peur, fait mal. Plus personne n'a l'audace de le tenter, et à long terme, c'est la banqueroute assurée du projet.
L'objectif de cet article est de proposer une approche efficace pour savoir comment attaquer efficacement un grosse refonte d'un code mal couvert par les tests. Cette tâche technique est divisée en plusieurs problématiques : comment déterminer les zones de risque du projet, trouver où poser les tests pour sécuriser l'étape de refactoring et trouver les zones à refactorer.
Pour rendre l'article concret, nous allons utiliser à titre d'exemple XDepend qui est un outil d'analyse de code statique java. En effet, on parle d'amélioration de la qualité, donc pour juger de l'efficacité de nos actions, il faut pouvoir les mesurer.
Déterminer et sécuriser les zones de risques du projet
Les méthodes les plus risquées sont premièrement les méthodes les plus utilisées: en effet, un bug introduit dans le refactoring d'une méthode utilisée dans la plupart des fonctionnalités de votre application et les impacts sont tout de suite démesurés.
Pour mettre en évidence ces méthodes , nous disposons de quelques métriques simples comme le couplage afférent (Ca) et le couplage efférent (Ce). Le couplage afférent pour un élément de code est le nombre d'éléments qui l'utilisent. Cette métrique témoigne de la "responsabilité" de cet élément dans l'ensemble du code. A l'opposé, le couplage efférent est le nombre d'éléments différents qu'utilise un élément, témoignant de son "indépendance" vis à vis du reste du code.
L'outil d'analyse de code statique que nous utilisons, XDepend, permet de sortir la liste des méthodes les plus utilisées en une simple requête :
SELECT TOP 10 METHODS ORDER BY MethodCa DESC
Les méthodes qui ressortent de ces métriques sont donc logiquement les premières sur lesquelles il faudra porter son attention.
XDepend permet d'aller encore plus loin dans cette recherche, en donnant un score à chaque méthode (MethodRank) et chaque type (TypeRank) selon son importance. Ce score est calculé à la manière du Pagerank de Google : tout comme la pertinence d'une page web, l'importance d'un élément de code est déterminée par le nombre d'éléments de code qui l'utilisent et de leur importance.
SELECT TOP 10 METHODS ORDER BY TypeRank DESC
Sécuriser l'existant
On peut ensuite filtrer la liste de méthodes ordonnée par importance avec les informations relatives à la couverture de tests existante (si celle-ci existe). Ainsi, on met en exergue les méthodes les plus importantes qui ne sont pas suffisamment testées :
SELECT TOP 10 METHODS WHERE PercentageCoverage < 98 ORDER BY MethodRank DESC
Poser des tests sur ces méthodes nous assurera que le refactoring des zones associées ne provoquera aucune régression.
Repérer les zones à refactorer
Une fois le coeur du projet renforcé par un harnais de tests, on peut sereinement envisager de casser morceau par morceau pour mieux reconstruire. Mais comment déterminer la pertinence des méthodes à refactorer? Comment mesurer le gain qu'apportera ce refactoring pour la suite du projet ? Voici quelques pistes classiques à envisager en premier lieu :
Les méthodes mal codées
La littérature logicielle regorge de préconisation permettant de s'assurer qu'une méthode reste maintenable en traçant des limites pertinentes : complexité cyclomatique, nombre de lignes de code, de paramètres, de variables...
Toutes ces limites peuvent être visualisées en une requête :
WARN IF Count > 0 IN SELECT TOP 10 METHODS WHERE ( NbLinesOfCode > 30 OR NbBCInstructions > 200 OR CyclomaticComplexity > 20 OR BCCyclomaticComplexity > 50 OR BCNestingDepth > 4 OR NbParameters > 5 OR NbVariables > 8 OR NbOverloads > 6 ) ORDER BY MethodRank DESC
Le manque de cohésion des méthodes (Lack of Cohesion in Methods, LCOM)
La notion de cohésion d'une classe est aussi importante : les responsabilités de la classe forment un tout cohérent. Pour déterminer la cohésion, on se base sur l'utilisation des variables d’instance par les méthodes de la classe. Plus le manque de cohésion (LCOM) est élevé, plus la classe est considérée complexe et donc moins facile à maintenir. Sur des classes comportant de nombreuses variables d'instance et de nombreuses méthodes, il est alors judicieux de surveiller ces classes et de les refactorer pour faire descendre le LCOM.
WARN IF Count > 0 IN SELECT TOP 10 TYPES WHERE LCOM > 0.8 AND NbFields > 10 AND NbMethods >10 ORDER BY LCOM DESC
Les dépendances cycliques entre packages
Les dépendances cycliques entre packages amènent à l'antipattern du plat de spaghetti qui ne permet pas de déterminer le qui, le quoi et le comment d’une modification de données. Cette faible indépendance fonctionnelle porte préjudice au critère de réutilisation du composant. Si A dépend de B qui dépend de C qui dépend de A, alors A ne peut plus être développé et testé indépendamment de B ou C. Ces packages forment une unité indivisibe, une sorte de super-composant qui a un coup bien supérieur en maintenabilité que la somme des 3 packages. Il faut donc repérer les dépendances cycliques et les casser.
WARN IF Count > 0 IN SELECT TOP 10 JARS WHERE ContainsPackageDependencyCycle
Le code mort
Le code mort peut devenir une plaie dans la maintenabilité d'un projet Un Ca (couplage afférent) à 0 signifie que dans le contexte de l'application, la méthode ou le type concerné n'est pas directement utilisé. On peut ainsi mettre en évidence rapidement des portions de codes qui ne semblent pas être utilisées en associant quelques règles CQL simples définissant une méthode non-utilisée :
WARN IF Count > 0 IN SELECT TOP 10 METHODS WHERE MethodCa == 0 AND !IsPublic
Attention : Les méthodes publiques peuvent faire partie d'une API exposée et donc logiquement ne pas être utilisées dans le contexte de l'application, il faut donc les écarter de cette recherche.
Mesurer l'impact d'une session de refactoring
L'idéal après un refactoring est de prendre le temps d'analyser ce que le refactoring a apporté. Première piste, regarder toutes les méthodes qui ont été modifiées suite au refactoring.
SELECT METHODS WHERE (CodeWasChanged OR WasAdded)
Le résultat de cette requête est parfois surprenant lorsqu'on compare ce qui était prévu et tout ce qui a été modifié au final. On peut aussi tracer le nombre de méthodes et de lignes de code qui ont été modifiées et opposer ces informations à la durée du refactoring. Garder ces traces permet de s'améliorer en estimant plus justement les refactoring suivants.
On doit aussi s'assurer que le code qui a été modifié est maintenant couvert au maximum. En effet, le but du refactoring est de "perdre du temps" pour remonter le niveau de qualité exigé pour maintenir le projet et être à l'avenir entièrement serein sur les points refactorés. La requête suivante permet cette détection :
WARN IF Count > 0 IN SELECT METHODS WHERE PercentageCoverage < 98 AND (CodeWasChanged OR WasAdded) ORDER BY PercentageCoverage DESC , NbLinesOfCodeCovered , NbLinesOfCodeNotCovered
(l'idéal est d'avoir un code couvert à 100%, mais en pratique il n'est pas toujours possible d'atteindre les 100% de couverture, 98% est notre valeur empirique). J'espère que ces quelques préconisations vous aideront dans vos refactoring afin que cette tâche apporte encore plus de valeur à votre projet. Et vous, quels sont les indicateurs que vous surveillez durant vos refactoring et quelles autres approches avez-vous pour détecter au mieux les douleurs dans votre code ?
Aparté sur l'outil
Les requêtes utilisées dans cet article sont du CQL (Code Query Language) et peuvent être exploitées par les outils d'analyse de code NDepend (pour .Net) et XDepend pour Java que nous utilisons pour auditer les projets de nos clients. Il suffit d'avoir les binaires du projet (.dll ou .jar) pour lancer une analyse et effectuer ces requêtes sur vos projets. La plupart des requêtes sont prédéfinies pour vous aider dans vos analyses mais la puissance du langage vous permet d'affiner les requêtes facilement et d'ajouter des conditions supplémentaires sur plus de 80 métriques de design logiciel. La force de cet outil réside dans sa GUI aussi riche que dynamique.