CHAPITRE 3 : QUALITÉ LOGICIELLE ET REFACTORING

Un logiciel qui marche n'est pas forcément un bon logiciel. Celui-ci fonctionne aujourd'hui et tiendra cinq ans ; celui-là fonctionne aussi, mais chaque correction y casse deux autres choses. La différence ne se voit pas à l'écran. Elle se voit le jour où il faut changer quelque chose. Ce chapitre donne les moyens de la nommer, de la mesurer, puis de la réduire.

Deux qualités, et non une

La première est celle que l'utilisateur constate : le logiciel rend le service attendu, assez vite, sans se tromper, sans perdre de données. On l'appelle qualité externe. Elle se teste, et le chapitre précédent s'est occupé de la tester.

La seconde ne se constate pas de l'extérieur : c'est la facilité avec laquelle on peut lire, comprendre et modifier le programme. On l'appelle qualité interne. Personne ne la réclame dans un cahier des charges, et c'est précisément pourquoi elle se dégrade. Elle n'envoie sa facture que plus tard, sous la forme d'un délai qui triple sur une demande qui paraissait simple.

Le lien entre les deux

La qualité interne n'est pas un luxe d'esthète. Un code que personne ne comprend plus est un code où les correctifs sont posés à l'aveugle, et donc un code dont la qualité externe finit par baisser aussi. La qualité interne est le taux d'intérêt auquel on emprunte pour livrer vite.

La norme ISO/IEC 25010 range la qualité d'un produit logiciel en huit caractéristiques. Elle a le mérite de donner un vocabulaire commun, et surtout d'obliger à choisir : on ne maximise pas les huit à la fois, on arbitre.

Les huit caracteristiques de qualite du modele ISO/IEC 25010 : aptitude fonctionnelle, performance, compatibilite, utilisabilite, fiabilite, securite, maintenabilite, portabilite, chacune avec ses sous-caracteristiques.

Dans un projet réel, les arbitrages sont explicites. Un logiciel embarqué dans un automate privilégie la fiabilité et la performance, et accepte une portabilité nulle. Une application interne de saisie privilégie l'utilisabilité et la maintenabilité, et se moque de tenir dix mille utilisateurs simultanés. Écrire l'arbitrage dans le dossier de conception évite de se faire reprocher plus tard une qualité qu'on n'avait jamais promise.

Mesurer la qualité interne

On ne pilote pas ce qu'on ne mesure pas, et la qualité interne se mesure mieux qu'on ne le croit. Quatre familles de mesures suffisent à la décrire honnêtement.

La complexité cyclomatique

Proposée par Thomas McCabe en 1976, elle compte le nombre de chemins d'exécution indépendants d'un morceau de code. Sur le graphe de flot de contrôle, elle vaut :

V(G) = E − N + 2, où E est le nombre d'arcs et N le nombre de nœuds.

En pratique on la calcule plus vite : un plus le nombre de points de décision. Chaque si, chaque tantque, chaque pour, chaque et ou ou dans une condition composée, chaque branche d'un selon ajoute un.

Graphe de flot de controle d'une fonction a trois points de decision : les noeuds, les arcs, le calcul V(G) = E moins N plus 2 et les quatre chemins independants correspondants.

La mesure sert à deux choses. Elle donne d'abord le nombre minimal de cas de test nécessaires pour couvrir toutes les branches, ce qui en fait un outil de test autant que de qualité. Elle signale ensuite les fonctions qu'un lecteur ne peut plus tenir dans sa tête.

Lecture usuelle de V(G) pour une fonction
V(G)LectureConduite à tenir
1 à 4SimpleRien à faire.
5 à 10RaisonnableSurveiller, tester chaque branche.
11 à 20Difficile à suivreDécouper, c'est presque toujours possible.
plus de 20Non testable en pratiqueDécouper avant toute modification.

Ces seuils ne sont pas une loi. Une fonction de validation qui enchaîne quinze contrôles indépendants et plats est à V(G) = 16 tout en restant parfaitement lisible. Le chiffre ouvre la discussion, il ne la tranche pas.

Le couplage et la cohésion

Deux notions que Larry Constantine a introduites ensemble, et qui se lisent ensemble. Le couplage mesure combien un module dépend des autres ; la cohésion mesure à quel point ce qu'il contient va ensemble.

La règle tient en une phrase : couplage faible, cohésion forte. Un module à cohésion faible est un fourre-tout, souvent nommé Utils ou Manager ; on ne sait jamais où y chercher. Un module à couplage fort ne peut ni être testé seul, ni être modifié seul, ni être remplacé.

La duplication et la taille

La duplication est la plus facile à mesurer et la plus coûteuse à ignorer : un correctif appliqué à une copie sur trois produit un bogue qui réapparaît sans explication. La taille, elle, se mesure par fonction et par fichier. Les chiffres importent moins que leur évolution : une classe qui gagne deux cents lignes par trimestre est en train de devenir le goulot du projet.

La dette technique

La métaphore est de Ward Cunningham, en 1992. Livrer vite en prenant des raccourcis, c'est emprunter : on obtient tout de suite ce dont on a besoin, et on paie des intérêts à chaque passage ultérieur dans ce code. La métaphore est utile parce qu'elle rend la décision discutable avec une direction : emprunter n'est pas une faute, emprunter sans le savoir et sans jamais rembourser en est une.

Martin Fowler distingue quatre situations, selon que la dette est délibérée ou non, prudente ou non.

Le quadrant de la dette technique
ImprudentePrudente
Délibérée « On n'a pas le temps de concevoir. » Le cas le plus destructeur, parce qu'il se répète. « On livre pour le salon, et on reprend la couche d'accès aux données en janvier. » Acceptable, si la reprise est planifiée.
Involontaire « On ne savait pas qu'il fallait séparer ces deux responsabilités. » Se traite par la formation et la revue. « Maintenant qu'on a fini, on voit comment il aurait fallu s'y prendre. » Inévitable, et sain : c'est de l'apprentissage.

La dette se chiffre. Un outil d'analyse statique produit un délai de remise en état, en jours, et un ratio de dette : le coût de remise en état divisé par le coût de réécriture complète. Ce ratio est la seule forme sous laquelle un comité de pilotage accepte généralement d'entendre parler de qualité interne.

Les odeurs de conception

Kent Beck a proposé le mot : une odeur n'est pas un bogue, c'est un indice. Elle ne prouve rien, elle invite à regarder. Les reconnaître vite est la compétence qui distingue une revue de code utile d'une revue de code polie.

Six odeurs qu'on rencontre tous les jours
OdeurCe qu'on observeTransformation
Fonction longuePlusieurs écrans, plusieurs niveaux d'indentation, des commentaires qui annoncent des sections Extraire une fonction, une par section annoncée
Classe tentaculaireUne classe qui sait tout, que tout le monde appelle, et qui change à chaque demande Extraire une classe par responsabilité
Code dupliquéLe même traitement à deux endroits, aux noms de variables près Extraire une fonction, puis la remonter
Liste de paramètres longueSix paramètres ou plus, qu'on passe toujours ensemble Introduire un objet paramètre
Envie de fonctionnalitéUne méthode qui n'utilise presque que les données d'une autre classe Déplacer la méthode vers cette classe
Conditionnel sur le typeUn selon sur un champ « type » répété en cinq endroits Remplacer le conditionnel par le polymorphisme

Le refactoring

Martin Fowler en donne une définition stricte, et la rigueur est ici tout l'intérêt du mot : modifier la structure interne d'un programme sans changer son comportement observable. Si le comportement change, ce n'est plus un refactoring, c'est une évolution ou une correction, et cela ne se conduit pas de la même manière.

La règle qui ne souffre aucune exception

On ne refactore pas du code qui n'est pas couvert par des tests. Sans tests, « sans changer le comportement observable » est une affirmation que rien ne vérifie, et l'opération devient un pari. Si le code à reprendre n'est pas testé, le premier travail est de l'entourer de tests, même grossiers, même lents.

Le refactoring se pratique par petits pas, chacun suivi d'une exécution des tests. Cette discipline paraît lente et elle est l'inverse : elle garantit qu'on sait toujours quelle modification a cassé quoi. Un refactoring mené en trois heures sans exécuter les tests se solde en général par un retour en arrière complet.

Avant et apres une extraction de methode : une fonction de quarante lignes a trois responsabilites melees devient une fonction de coordination appelant trois fonctions nommees, avec la complexite cyclomatique de chacune.

Les transformations utiles se comptent sur les doigts de deux mains, et extraire une fonction est de loin la plus rentable. Viennent ensuite renommer, pour que le nom dise ce que la chose fait ; déplacer une méthode vers la classe dont elle utilise les données ; introduire un objet paramètre ; remplacer un nombre magique par une constante nommée ; remplacer un conditionnel sur le type par le polymorphisme ; et séparer une requête d'une commande, c'est-à-dire ne jamais écrire dans une fonction qui est là pour renvoyer une valeur.

Cinq principes qui guident le découpage

Robert Martin les a regroupés sous l'acronyme SOLID. Ils ne sont pas des règles à appliquer partout, mais des critères pour juger un découpage.

  • Responsabilité unique. Un module ne doit avoir qu'une raison de changer. Le test pratique : énumérez les demandes qui obligeraient à le modifier. S'il y en a trois sans rapport entre elles, coupez.
  • Ouvert, fermé. On doit pouvoir ajouter un cas sans modifier le code existant. C'est ce que permet le polymorphisme là où un selon obligerait à rouvrir cinq fichiers.
  • Substitution de Liskov. Un sous-type doit pouvoir remplacer son type de base sans surprise. Une sous-classe qui refuse une opération que la classe mère accepte viole le principe.
  • Séparation des interfaces. Mieux vaut trois interfaces étroites qu'une interface large dont chaque client n'utilise qu'un tiers.
  • Inversion des dépendances. La logique métier ne doit pas dépendre de la base de données ni du format de fichier, mais d'une abstraction que ceux-ci réalisent. C'est ce qui rend la logique testable sans infrastructure.

Les garde-fous qu'on outille

Trois dispositifs tiennent la qualité interne dans la durée, et aucun ne remplace les deux autres.

L'analyse statique lit le code sans l'exécuter et signale ce qui se détecte mécaniquement : duplication, complexité, variables inutilisées, constructions dangereuses. Un linter fait la part syntaxique, un outil comme SonarQube y ajoute les métriques et le suivi dans le temps.

La revue de code traite ce qu'aucun outil ne voit : le nom qui induit en erreur, la responsabilité mal placée, le cas métier oublié. Elle vaut par sa régularité et par sa petite taille. Une revue de quarante lignes trouve des problèmes ; une revue de mille lignes n'engendre qu'une approbation de principe.

La porte de qualité transforme le constat en décision. On fixe des seuils sur le code nouveau ou modifié, et non sur la totalité du dépôt : aucune duplication nouvelle, couverture des lignes nouvelles au moins égale à 80 %, aucune vulnérabilité ajoutée. Un seuil posé sur l'existant est ingérable sur une base ancienne et sera désactivé dans le mois. Un seuil posé sur le nouveau code est tenable immédiatement, et il suffit à faire remonter la qualité au rythme des modifications.

Chez NKILItech

NKILItech reprend l'application de facturation de ses prestations, écrite il y a six ans. L'analyse statique donne les chiffres suivants : 31 000 lignes, 9,4 % de duplication, une couverture de 12 %, et une fonction traiterFacture de 340 lignes à V(G) = 47 que personne n'ose toucher.

La tentation est la réécriture. C'est presque toujours une erreur : la réécriture repart de zéro sur un périmètre qu'on connaît mal, elle gèle les évolutions pendant des mois, et elle redécouvre un par un les cas particuliers que l'ancien code traitait sans le dire.

La conduite raisonnable tient en quatre temps. Entourer d'abord traiterFacture de tests de caractérisation, c'est-à-dire de tests qui ne décrivent pas ce que le code devrait faire mais ce qu'il fait réellement, jeu de données réelles en entrée et résultat observé en sortie. Extraire ensuite, un par un, les blocs identifiables : le calcul de la remise, l'application de la taxe, la mise en forme du document. Poser une porte de qualité sur le code nouveau uniquement. Et traiter la dette au fil des demandes, en appliquant la règle du boy-scout : on laisse le fichier qu'on a ouvert un peu plus propre qu'on ne l'a trouvé.

Au bout de six mois, la duplication était à 4 %, la couverture du code modifié dépassait 70 %, et traiterFacture était devenue une fonction de coordination de 24 lignes. Aucune évolution n'avait été gelée.

Exercice d'application

Reprendre un calcul de remise chez NKILItech

Voici, en pseudo-code, la fonction qui calcule la remise accordée sur une prestation NKILItech. Elle est appelée depuis trois endroits de l'application.

fonction remise(client, montant, type, anciennete, enCours)
  r = 0
  si type = "MAINTENANCE" alors
    si anciennete >= 24 et montant > 500000 alors
      r = montant * 0.15
    sinon si anciennete >= 12 alors
      r = montant * 0.08
    sinon
      r = montant * 0.03
    finsi
  sinon si type = "INSTALLATION" alors
    si montant > 1000000 alors
      r = montant * 0.10
    sinon
      r = montant * 0.05
    finsi
  sinon si type = "FORMATION" alors
    r = montant * 0.07
  finsi
  si enCours > 3 alors
    r = r + montant * 0.02
  finsi
  si r > montant * 0.20 alors
    r = montant * 0.20
  finsi
  journaliser("remise " + client + " = " + r)
  retourner r
fin
  1. Calculez la complexité cyclomatique de cette fonction. Détaillez votre décompte.
  2. Nommez trois odeurs de conception présentes, en citant pour chacune la ligne ou le bloc concerné.
  3. La direction annonce l'ouverture d'un quatrième type de prestation, « AUDIT ». Quel principe SOLID la fonction actuelle viole-t-elle, et que coûte cette violation ?
  4. Proposez un découpage. Donnez les signatures des fonctions que vous créez et la complexité cyclomatique de chacune.
  5. Avant d'appliquer ce découpage, quelle précaution est obligatoire ? Indiquez combien de cas de test au minimum, et justifiez ce nombre.
Voir le corrigéMasquer

1. Complexité cyclomatique. On compte un, puis un par point de décision :

  • type = "MAINTENANCE" : 1
  • anciennete >= 24 et montant > 500000 : 2, car le et compte pour un point de décision supplémentaire
  • anciennete >= 12 : 1
  • type = "INSTALLATION" : 1
  • montant > 1000000 : 1
  • type = "FORMATION" : 1
  • enCours > 3 : 1
  • r > montant * 0.20 : 1

Total des points de décision : 9. V(G) = 1 + 9 = 10. La fonction est à la limite haute du raisonnable, et elle mélange déjà plusieurs sujets.

2. Trois odeurs.

  • Conditionnel sur le type. La cascade sur type est l'odeur principale : le comportement dépend d'un champ qui joue le rôle d'un type, ce qui est le travail du polymorphisme.
  • Nombres magiques. 24, 12, 500000, 1000000, 3, et les sept taux, tous écrits en clair. Personne ne peut dire, en lisant, si 0,15 est une règle commerciale ou une coquille.
  • Fonction qui fait plus que son nom. remise calcule, mais aussi plafonne, et surtout journalise. Un effet de bord dans une fonction de calcul viole la séparation entre requête et commande, et empêche de la tester sans examiner le journal.

On pourrait en ajouter deux : la liste de cinq paramètres, dont quatre décrivent le même client, et le fait que la majoration enCours s'applique même quand aucun type n'a été reconnu, ce qui accorde une remise sur un type inconnu.

3. Le principe violé. C'est le principe ouvert, fermé : ajouter « AUDIT » oblige à rouvrir et à modifier une fonction qui fonctionne, donc à risquer les trois types existants. Le coût n'est pas la ligne à écrire, il est dans la non-régression : il faut retester les trois types déjà en service, et les trois appelants. Le principe de responsabilité unique est touché aussi : la fonction change pour une raison commerciale, pour une raison de plafonnement et pour une raison de journalisation.

4. Découpage proposé. On sépare le choix de la règle, le calcul de chaque règle, la majoration, le plafonnement et la journalisation.

interface RegleRemise
  fonction taux(prestation) : reel          /* V(G) varie par regle */

classe RemiseMaintenance implemente RegleRemise  /* V(G) = 3 */
classe RemiseInstallation implemente RegleRemise /* V(G) = 2 */
classe RemiseFormation implemente RegleRemise    /* V(G) = 1 */

fonction regleFor(type) : RegleRemise       /* V(G) = 1, un dictionnaire */
fonction majorationFidelite(enCours) : reel /* V(G) = 2 */
fonction plafonner(remise, montant) : reel  /* V(G) = 2 */
fonction remise(prestation) : reel          /* V(G) = 1 */

remise devient : obtenir la règle, appliquer son taux, ajouter la majoration, plafonner, retourner. Aucune décision, donc V(G) = 1. La journalisation sort de la fonction et revient à l'appelant ; les seuils et les taux deviennent des constantes nommées, ou mieux, des données de configuration, car ce sont des règles commerciales qui changeront sans qu'on veuille recompiler.

Somme des complexités : 12, contre 10 avant. Le total augmente légèrement, et c'est normal : ce qui compte est que la complexité maximale par unité passe de 10 à 3, et que l'ajout d'« AUDIT » devienne une classe nouvelle sans toucher à l'existant.

5. La précaution obligatoire. Des tests de caractérisation avant toute modification : on fige le comportement actuel, y compris ses éventuels défauts, pour pouvoir affirmer ensuite que le refactoring n'a rien changé.

Le minimum est donné par la complexité cyclomatique : 10 cas de test, puisque V(G) = 10 majore le nombre de chemins indépendants et donne donc le nombre de cas nécessaires pour couvrir toutes les branches. En pratique on en écrira davantage, pour ajouter les valeurs limites de chaque seuil : ancienneté à 11, 12, 23 et 24 mois, montant à 500 000 et 500 001, nombre de prestations en cours à 3 et 4, et un cas où le plafond de 20 % mord effectivement.

Pour vous évaluer. Un sujet d'examen complet accompagne ce chapitre : un QCM de dix questions, deux exercices et un problème d'entreprise, le tout corrigé.

Examen Qualité logicielle et refactoring : Génie logiciel avancé

Questions / Réponses

Aucune question. Soyez le premier à poser une question.
★ ★ ★ ★ ★
2 votes. Moyenne 5 sur 5.

Ajouter un commentaire

Anti-spam