Application Legacy – Refactoring ou reengineering? (VII)

Qualilogy Legacy Application Refactoring ReengineeringNous avons montré dans le post précédent comment utiliser le dashboard SonarQube afin d’estimer l’effort de tests de caractérisation, tests préconisés par Michael Feathers dans son livre ‘Working Effectively with Legacy Code’.

Nous avons catégorisé les différents composants de notre application Legacy (Microsoft Word 1.1a) en différents groupes, des fonctions les plus simples avec une Complexité Cyclomatique (CC) inférieure à 20 points, aux fonctions complexes à très complexes, jusqu’à 200 points de CC, et finalement 6 composants ‘monstres’.

Nous avons construit une formule basée sur la Complexité Cyclomatique et un facteur de lisibilité afin d’évaluer l’effort de test sur chacun de ces groupes.

Appréciation des résultats

Nous avons obtenu un résultat total de 234 jours, qui se répartissent de la façon suivante :

Tableau 1 – Distribution de l’effort de tests par catégorie de composants 
Qualilogy-Legacy-TestEffort-Synthesis

  •  93.5 jours pour une couverture de 60% des fonctions avec une CC inférieure à 20 points, représentant 40% de l’effort total et 44% de la Complexité Cyclomatique totale de l’application.
  • 92 jours (39% du temps total) pour les 503 fonctions entre 20 et 100 points de CC, représentant 44% de la CC totale.
  • 25 j (11% du temps total) pour les 30 fonctions entre 100 et 200 points de CC, représentant 9% de la CC totale.
  • 23.5 j ou 10% de l’effort total pour les 6 fonctions les plus complexes, représentant 3% de la CC de l’ensemble de l’application.

Cet effort nous permet une couverture de tests de 65.5% des fonctions, soit les 2/3 de notre application.

J’avais passé pas mal de temps sur la formule de calcul de l’effort de test, de manière à obtenir une règle d’évaluation qui soit logique, cohérente, et donc compréhensible, en se basant sur le nombre de chemins logiques dans une fonction, représenté par la Complexité Cyclomatique, ainsi que la plus ou moins grande facilité de lecture. C’est ainsi que procéderait un développeur auquel on demanderait d’estimer le nombre de jours qui lui serait nécessaire pour développer des tests unitaires sur un code qu’il ne connaît pas.

Je voulais également que la règle soit précise (tout en restant simple), de manière à pouvoir moduler la formule en fonction de la taille et la complexité des composants. J’ai joué un peu avec les différents paramètres ‘Readibility Factor%’ et ‘Characterization Test time’ afin de parvenir à un résultat unitaire pour chaque catégorie de composants qui me paraisse satisfaisant : 54 minutes d’écriture de tests pour une fonction entre 12 et 20 points de CC, 120 minutes pour une fonction entre 40 et 50 points de CC et 200 à 300 lignes de code, 240 minutes (soit une demi-journée) pour une fonction entre 60 et 70 points de CC et entre 500 et 700 lignes de code, etc.

Une fois les différents tableaux réalisés avec cette formule, ma première réaction au vu des résultats finals, a été de trouver ceux-ci assez réalistes. 234 jours, pour une couverture de tests des 2/3 des 3 936 fonctions répartis dans 349 programmes C : cela ne me paraît pas invraisemblable.
Je pense même que ce n’est pas sous-évalué, si on considère que les temps de réalisation de tests sur les composants les plus simples (< 20 CC) à moyennement complexe (20 < CC < 50) sont, je pense, estimés assez généreusement. Au-delà, pour les composants les plus lourds, j’ai du mal à me faire une idée très précise de la justesse de notre évaluation. Mais 3 jours pour une fonction très complexe avec environ 200 points de CC et 750 lignes de code ne me paraissent pas non plus incohérents.

La répartition de l’effort de test me semble également intéressante :

  • Deux chiffres très proches d’environ 90 jours pour chacune des deux premières catégories, avec environ 19 000 points de CC pour chaque catégorie, sur 3 400 composants simples et également pour 500 composants complexes.
  • Deux chiffres proches à nouveau, de 25 jours pour 30 composants très complexes et 23 jours pour 6 ‘monster components’.

Quand je dis que ces résultats me paraissent réalistes, je ne veux pas dire qu’ils sont d’une précision absolue et d’une fiabilité et à toute épreuve, sinon qu’ils peuvent supporter quelques objections.

Objections

J’avais terminé le post précédent en demandant si cette estimation semblait correcte et les réactions ont été plutôt positives. On m’a dit que c’était ‘Ok’, et que la ‘scalability’, c’est-à-dire la variation d’échelle entre les différentes catégories, croissante en fonction de la CC, semblait assez juste.
Néanmoins, en tant que consultant, je dois me préparer à toute objection lors de la présentation de ces résultats à un client. Quelles pourraient être celles-ci ?

Un responsable fonctionnel ou utilisateurs de l’application (stakeholder) : «  Je ne comprends pas : vous dites qu’il faut 4 minutes par point de complexité pour les composants les plus simples et 2 minutes par point de complexité pour les composants les plus complexes. Est-ce que ce ne devrait pas être l’inverse ? ».

En fait, l’écriture d’un test va nécessiter le développement d’une fonction spécifique à ce test, afin de vérifier une ou plusieurs valeur(s) pour chaque chemin logique et donc chaque point de complexité. Si je n’ai qu’un unique point de CC, je vais passer moins de temps à écrire une ligne de test et proportionnellement plus de temps à développer la fonction, à la compiler, à l’exécuter, à vérifier le résultat du test, éventuellement à modifier la fonction et la tester à nouveau. Si j’ai plusieurs points de CC, je vais passer un peu plus de temps pour tester différentes valeurs et proportionnellement moins de temps dans l’écriture et l’exécution de la fonction.

D’ailleurs, on ne constate pas vraiment de gap lorsque l’on passe d’une fonction simple entre 12 et 20 points de CC et 54 minutes de temps de réalisation des tests, à une fonction moyennement complexe entre 20 et 30 points de CC, et 50 à 60 minutes de tests (en fonction de taille de la fonction, en lignes de code).

Un membre de l’équipe de projet : « Votre formule me paraît correcte, mais je pense que le Readibility Factor n’est pas assez progressif. D’ailleurs, vous passez brusquement le RF% de 4 à 10 pour les fonctions extrêmement complexes ».

Je suis d’accord. Afin de disposer également d’une seconde hypothèse, d’une fourchette haute pour notre évaluation, j’ai rehaussé ce facteur de la manière suivante :

  • Pour les fonctions inférieures à 100 lignes de code (LOC), je laisse le RF% = 1.
  • De 100 à 200 LOC, le RF% passe de 1.5 à 2.
  • De 200 à 300 LOC, le RF% passe de 2 à 3.
  • De 300 à 500 LOC, le RF% passe de 2.5 à 4.
  • De 500 à 700 LOC, le RF% passe de 4 à 6.
  • Au-delà de 700 LOC, j’ai conservé les paramètres actuels.

Voici le nouveau tableau obtenu avec ces paramètres :

Tableau 2 – Calcul de l’effort de test sur les fonctions très complexes (hypothèse haute)

Qualilogy-Legacy-TestEffort-CCSup20_NewRF
Nous passons de 117.2 jours à 131.8 jours, soit une augmentation de 14.6 jours, ou encore un effort supplémentaire d’environ 12% de la charge initiale. Donc l’impact de cette modification est relativement modéré. On constate d’ailleurs qu’il est plus important sur les composants de taille plus élevée (c’est logique), mais qui ne sont pas les plus nombreux. Par exemple, on passe de 4 heures à 5 heures pour un composant de 60 à 70 points de CC avec plus de 500 LOC, mais comme nous n’en avons qu’un seul dans l’application …

Le CIO : « Quelle fiabilité accordez-vous à vos résultats ? Dans quelle mesure puis-je me baser sur ces chiffres afin de décider de lancer un outsourcing pour cette application ? ».

Je vais préciser cela en vous présentant le plan d’actions.

Plan d’actions

Nous avons un total de 234 jours pour cette opération de transfert de connaissances, ou 248.8 jours si on modifie le paramètre RF% comme précédemment, ce qui représente entre 11.7 et 12.5 mois / personne sur la base de 20 jours par mois. Nous allons baser notre plan d’actions sur ces deux hypothèses, la première ‘hypothèse moyenne’, la seconde ‘hypothèse haute’.

En fait, il ne faut pas compter 20 jours par mois, car cela suppose que personne ne s’absente ou ne prenne de congés. On considère (en France) qu’une personne bénéficie de 5 semaines de congés et 2 semaines de jours fériés. Ceci nous donne donc un total de :

52 semaines par an – 7 semaines d’absence = 45 semaines travaillées.
45 semaines x 5 jours par semaine = 225 jours de travail par an.
225 jours travaillés / 12 mois = 18.5 jours travaillés par mois.

Notre charge totale de projet est de 234 jours (hypothèse 1) ou 248.8 jours (hypothèse 2) ce qui représente respectivement 2.5 ou 2.65 mois / hommes pour une équipe de 5 personnes et 18.5 jours de travail par mois.
5 personnes me paraissent une équipe de taille correcte pour reprendre cette application et en assurer la maintenance future : je doute que l’équipe originale de Microsoft ait été moins nombreuse. Sur la base de cette équipe, notre projet de transfert de connaissances serait inférieur à 3 mois.

Or 3 mois est la durée systématiquement utilisée pour une phase de ‘knowledge transfer’, dans toute réponse à un appel d’offres ou RFP. En dessous de 3 mois, ce n’est pas crédible : il faudrait un nombre de personnes plus important pour assurer ce transfert que le nombre de personnes nécessaire lors des phases suivantes de maintenance correcte et évolutive. Ce qui voudrait dire, soit ne pas compléter correctement cette phase de transfert, et donc compromettre énormément la prestation d’outsourcing, soit  former des personnes sur une application qu’ils abandonneront ensuite. Dangereux et stupide dans les deux cas.
Au-delà de 3 mois, vous prenez le risque d’alourdir la charge et donc le coût du projet au-delà de ce que proposeront vos concurrents, et par conséquence de perdre le contrat. Donc tout le monde va proposer un délai de 3 mois, et ce délai est tout à fait acceptable pour un CIO.

Nous allons ainsi baser notre plan d’action sur un planning de 3 mois, avec une charge de 2.5 (ou 2.65 mois /homme) pour une équipe de 5 personnes. Ceci nous donne une marge de 20% (3 – 2.5 = 0.5 = 20% de 2.5) ou 13% (pour une charge de 2.65 mois / hommes).

Le tableau suivant synthétise ces résultats :

Qualilogy_Legacy_ActionPlanTable

On évalue généralement la charge de gestion du projet à 20% du total projet, mais je pense que sur une phase de transfert de connaissances, le besoin de gestion de projet sera inférieur. Dans tous les cas, nous devrions bénéficier d’un peu de marge de manœuvre, si nous partons sur un planning de 3 mois.

Donc je ferai les propositions suivantes :

Commencer par les fonctions les plus complexes

Les 30 fonctions très complexes nécessitent 25.1 jours dans notre hypothèse 1, ou 26.9 dans notre hypothèse 2. Les 6 fonctions ‘monster’ nécessitent 23.5 j. Au total, je vais arrondir à 50 jours de charge, soit 10 jours / homme pour une équipe de 5 personnes.

Notre conseil est d’utiliser les deux premières semaines du planning afin d’écrire les tests et de documenter les fonctions les plus complexes, avec des points d’avancement réguliers (au moins 2 par semaine) afin de vérifier si cette première partie du planning peut être tenue, normalement en 2 semaines. Si c’est le cas, nous pouvons être confiants pour la suite de notre projet de transfert de connaissances et d’écriture de tests.

Partager les tâches entre les deux équipes

Une autre recommandation que j’aimerais suggérer pour cette première phase : diviser le travail de développement de tests sur ces composants les plus complexes entre les deux équipes, l’actuelle qui connaît déjà le code, et la seconde vers laquelle doit s’effectuer le transfert de connaissance. Nous pouvons travailler de deux manières :

  • Développement conjoint : un développeur de la nouvelle équipe écrit les tests avec l’aide d’un développeur de l’actuelle équipe.
  • Développement séparé avec partage de connaissances : chaque développeur programme chacun de son côté des tests sur les fonctions les plus complexes (pas les mêmes en même temps, évidemment). Les deux se retrouvent en fin de journée afin de présenter leur travail, le commenter, l’expliquer et ainsi faciliter le transfert de connaissances vers le développeur de la nouvelle équipe.

La première option est probablement celle qui permettra une qualité optimale de notre couverture de tests, puisque nous allions les bénéfices de la découverte et de la compréhension du code par un développeur de la nouvelle équipe, avec l’aide d’un développeur qui connaît déjà ce code. L’inconvénient est qu’elle nécessite des ressources additionnelles de la part de l’équipe actuelle, non directement productives comme dans la seconde option.

Je dois dire que j’ai une préférence pour cette seconde option, qui me paraît présenter plus d’avantages. Le développeur de la nouvelle équipe effectue des tests de caractérisation qui lui permettent de découvrir et comprendre le comportement de l’application. En parallèle, un développeur de l’équipe actuelle écrit des tests unitaires sur un code qu’il connaît, avant de passer celui-ci au développeur de la nouvelle équipe. Il est important cependant de réserver du temps afin que chacun partage son travail et que le développeur de l’équipe actuelle, soit vérifie et complète le travail du développeur de la nouvelle équipe, soit lui explique les tests qu’il a lui-même développés. Ce temps de partage doit également permettre de documenter les tests réalisés. Je pense qu’une heure par jour de travail en commun, au début, devrait permettre ce partage de connaissances. Il faudra probablement passer à 2 heures par jour assez rapidement toutefois.

Dans les deux cas, cette répartition des tâches doit permettre :

  • D’assurer le transfert de connaissance et la qualité des tests sur les fonctions les plus complexes.
  • Un meilleur suivi sur cette phase de projet, probablement la plus délicate pour l’avenir.
  • D’assurer le planning et d’éviter tout dérapage au début du projet, en bénéficiant de la connaissance du code de l’équipe actuelle.

Moduler les ressources de manière la plus efficace

Dans les deux cas, nous aurons besoin de ressources de la part de l’équipe actuelle, mais encore une fois, sur une période relativement courte de deux semaines environ. Il est cependant possible de moduler, plus facilement d’ailleurs dans notre seconde option, en employant moins de développeurs de l’équipe actuelle. Nous pouvons même mixer les deux options : par exemple, développement conjoint pour les 6 fonctions ‘monstres’ et développement séparé pour les 33 autres fonctions très complexes.

Je pense que c’est le type de recommandation que nous pouvons proposer en comité de projet, et voir ce qu’en pensent les différents participants. Entre développement partagé ou séparé, avec plus ou moins de ressources différentes. En fait, toutes les variantes possibles sont imaginables.
Par exemple : 2 développeurs de l’équipe actuelle et 3 développeurs de la nouvelle équipe, pour un total de 5 personnes, se partagent le travail d’écriture de tests sur ces composants les plus complexes. Pendant le même temps, les 2 autres développeurs de la nouvelle équipe commencent à travailler sur les composants ‘moins’ complexes. Ceci permettrait :

  • D’assurer le planning sur la caractérisation des composants les plus complexes, avec 2 personnes qui en ont la connaissance …
  •  … tout en acquérant un premier aperçu du travail d’écriture de tests sur les composants entre 20 et 200 points de CC, pour une meilleure visibilité sur l’ensemble de l’application et donc du projet.

En fonction des différents choix, il faudra évidemment calculer la charge additionnelle pour l’équipe actuelle et le planning.

Modifier la couverture de tests

Je pense que débuter par les composants les plus complexes, en visant une couverture complète de ces composants égale à 100% de leur Complexité Cyclomatique devrait permettre d’assurer le transfert de connaissances sur la partie la plus difficile, la plus lourde et la plus complexe de l’application, et donc du projet. Ensuite, nous pouvons passer aux 503 fonctions de – relativement – moindre complexité.

Que se passe-t-il cependant si nous prenons du retard ? Si nous souhaitons respecter le planning de 3 mois que nous nous sommes assignés, il nous faudra diminuer la charge de travail et donc la couverture de tests.

Nous avons évalué celle-ci à 66% de la Complexité Cyclomatique de l’application, 100% pour les composants de plus de 20 CC et 60% pour les autres. Nous avons dit dans notre post précédent que l’effort de test correspondait (plus ou moins) à une loi de Pareto, et qu’il fallait à peu prés autant de temps pour développer 70% ou 80% des tests pour un composant que pour les 20% ou 30% restants. C’est à relativiser, mais une diminution mineure de notre couverture de tests devrait nous permettre de diminuer la charge travail de manière proportionnellement plus importante.

Par exemple, si au lieu de 100% de couverture des composants entre 20 et 200 points de CC, nous planifions une couverture moindre de 80%, notre charge de travail évolue de la manière suivante :

  • 93.7 jours au lieu de 117.2 jours dans notre hypothèse 1, soit 23.5 jours, soit un gain de 20% de la charge initiale.
  • 105.4 jours au lieu de 131.8 jours dans notre hypothèse 2, soit 26.4 jours pour un gain encore une fois de 20%.

En fait, on peut moduler là encore de différentes manières. Par exemple, 80% de couverture pour les composants de moins de 50 points de CC et 100% pour ceux entre 50 et 200 points de CC nous donnent les résultats suivants :

  • 105 jours au lieu de 117.2 jours dans notre hypothèse 1, soit un gain de 12.2 jours, environ 10% de la charge initiale.
  • 118.7 jours au lieu de 131.8 jours dans notre hypothèse 2, soit 13.1 jours pour environ 10% de gain.

On voit donc qu’en fonction d’un retard éventuel, il est possible de réduire celui-ci en gagnant des jours contre une réduction minimale de la couverture de tests, et donc de la qualité du transfert de connaissances.

Synthèse

Une des missions qui nous est assignée consiste à calculer le coût de transfert de connaissance de cette application vers une autre équipe et proposer une stratégie pour un tel projet.

Nous nous sommes appuyés sur la notion de tests de caractérisation de Michael Feathers, avec le double avantage de disposer d’une couverture de tests pour notre application Legacy et d’en assurer le transfert de connaissances.

Nous avons construit une formule basée sur la Complexité Cyclomatique ainsi qu’un facteur de lisibilité, en tentant de trouver le bon compromis entre simplicité et précision. Nous avons examiné les objections possibles quant aux résultats obtenus avec cette formule.

Enfin, nous avons bâti un plan d’actions, avec différentes propositions qui permettent de convaincre (ou de rassurer) un CIO de la fiabilité de notre analyse et de la faisabilité de notre projet.

Dans notre prochain post, nous travaillerons notre deuxième scénario : le refactoring de notre application Legacy, avec l’aide du plugin Sqale de SonarQube.

Ce article est également disponible en Leer este articulo en castellano et Read that post in english.

Laisser un commentaire

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