- Accueil
- TUTORIELS
- GÉNIE LOGICIEL AVANCÉ
- CHAPITRE 3
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.
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.
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.
| V(G) | Lecture | Conduite à tenir |
|---|---|---|
| 1 à 4 | Simple | Rien à faire. |
| 5 à 10 | Raisonnable | Surveiller, tester chaque branche. |
| 11 à 20 | Difficile à suivre | Découper, c'est presque toujours possible. |
| plus de 20 | Non testable en pratique | Dé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.
| Imprudente | Prudente | |
|---|---|---|
| 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.
| Odeur | Ce qu'on observe | Transformation |
|---|---|---|
| Fonction longue | Plusieurs écrans, plusieurs niveaux d'indentation, des commentaires qui annoncent des sections | Extraire une fonction, une par section annoncée |
| Classe tentaculaire | Une 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 longue | Six 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 type | Un 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.
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
selonobligerait à 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
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
- Calculez la complexité cyclomatique de cette fonction. Détaillez votre décompte.
- Nommez trois odeurs de conception présentes, en citant pour chacune la ligne ou le bloc concerné.
- 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 ?
- Proposez un découpage. Donnez les signatures des fonctions que vous créez et la complexité cyclomatique de chacune.
- 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": 1anciennete >= 24 et montant > 500000: 2, car leetcompte pour un point de décision supplémentaireanciennete >= 12: 1type = "INSTALLATION": 1montant > 1000000: 1type = "FORMATION": 1enCours > 3: 1r > 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
typeest 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.
remisecalcule, 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
Ajouter un commentaire