Chapitre 718 minIntermédiaire
Qualité du code et dette technique
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- définir la dette technique et expliquer ses « intérêts » ;
- citer les critères de maintenabilité d'un logiciel ;
- mettre en place un linter, un formateur et une analyse statique ;
- décider quand rembourser une dette et comment la suivre.
4 exercices corrigés1 schéma
La dette technique désigne l'ensemble des raccourcis pris dans le code (une conception bâclée, du code dupliqué, des tests absents, des dépendances jamais mises à jour) qui font gagner du temps aujourd'hui mais rendent chaque modification future plus lente et plus risquée. Comme une dette financière, elle a un capital, le travail qu'il faudrait pour la rembourser, et des intérêts, le temps perdu à chaque intervention tant qu'elle n'est pas remboursée. Ce chapitre définit la qualité d'un logiciel, explique comment la dette se forme, comment la mesurer avec des outils (linters, analyse statique, couverture de tests) et comment décider de la rembourser.
Ce qu'est un code de qualité
Un code de qualité n'est pas seulement un code qui marche. La norme internationale ISO/IEC 25010 décrit la qualité d'un produit logiciel par neuf caractéristiques (dans sa version révisée en 2023).
| Caractéristique | Ce qu'elle mesure | Sur Sorbier Frais |
|---|---|---|
| Aptitude fonctionnelle | L'application fait ce qui est demandé, et juste | Les totaux sont exacts |
| Efficacité de performance | Temps de réponse, ressources consommées | La liste des notes s'affiche en moins d'une seconde |
| Compatibilité | Fonctionner avec d'autres systèmes | L'export CSV s'ouvre dans le logiciel comptable |
| Capacité d'interaction | Facilité d'usage, accessibilité | Un salarié saisit une note sans formation |
| Fiabilité | Disponibilité, tolérance aux pannes | L'application reste accessible en fin de mois |
| Sécurité | Confidentialité, intégrité, traçabilité | Un salarié ne voit pas les notes des autres |
| Maintenabilité | Facilité à modifier le logiciel | Une évolution ne demande pas de tout relire |
| Flexibilité | Facilité à changer d'environnement | L'API peut passer d'un serveur à un conteneur |
| Sûreté | Absence de danger pour les personnes et les biens | Peu concernée ici |
La maintenabilité est la caractéristique de ce cours. La norme la décompose en cinq sous-caractéristiques, qui sont autant de questions à te poser sur un code :
- Modularité : un changement dans un module oblige-t-il à en modifier d'autres ?
- Réutilisabilité : une partie peut-elle servir ailleurs sans copier-coller ?
- Analysabilité : trouve-t-on vite la cause d'un bug, ou la partie à modifier ?
- Modifiabilité : peut-on changer le code sans introduire de défauts ?
- Testabilité : peut-on vérifier facilement qu'une partie fonctionne ?
Le référentiel du BTS SIO demande d'« évaluer la qualité d'une solution applicative ». Ces cinq questions, appuyées sur les mesures de ce chapitre, te donnent une grille pour le faire.
La dette technique
L'expression vient de Ward Cunningham, un développeur américain, en 1992. Livrer un code imparfait, expliquait-il, c'est comme contracter une dette : cela permet d'aller plus vite tout de suite, à condition de la rembourser rapidement en réécrivant ce qui doit l'être. Une dette jamais remboursée accumule des intérêts : chaque minute passée à contourner un code mal conçu en est un.
Sur Sorbier Frais, la dette prend des formes concrètes :
- la fonction de calcul des remboursements fait 40 lignes de
ifimbriqués, avec des montants écrits en dur (le chapitre 8 la reprend) ; - la conversion des montants est recopiée dans trois fichiers ;
- l'export CSV n'a aucun test ;
- le pilote de base de données a deux versions majeures de retard ;
- le README décrit une installation qui ne fonctionne plus.
Aucun de ces points n'empêche l'application de fonctionner aujourd'hui. Tous ralentissent la prochaine intervention, et augmentent le risque qu'elle casse quelque chose.
La dette n'est pas toujours une faute. Le développeur Martin Fowler la range dans un tableau à quatre cases, selon qu'elle est délibérée ou involontaire, prudente ou imprudente :
- Délibérée et prudente : on livre une version simplifiée pour tenir une échéance, en sachant ce qu'on sacrifie, et on crée le ticket pour y revenir. C'est un choix défendable.
- Délibérée et imprudente : on bâcle en sachant qu'on bâcle, sans plan pour réparer. C'est celle qui coûte le plus.
- Involontaire et imprudente : on ne savait pas faire mieux, faute de connaissances. Elle se réduit par la formation et la relecture.
- Involontaire et prudente : on a bien travaillé, mais on a appris en chemin ; avec le recul, on concevrait autrement. Elle est inévitable dans tout projet qui vit.
Les symptômes d'une dette qui coûte
Tu reconnais une dette qui pèse à ses effets, plus qu'au code lui-même :
- chaque évolution prend plus de temps que la précédente, pour des demandes de taille comparable ;
- les mêmes bugs reviennent, ou une correction en provoque une autre ailleurs ;
- certaines parties font peur : « n'y touche pas, ça marche, on ne sait pas pourquoi » ;
- un nouvel arrivant met des semaines à être autonome ;
- les tests sont absents ou ignorés, parce qu'ils échouent depuis longtemps ;
- les dépendances sont anciennes, et les mettre à jour paraît impossible.
Mesurer la qualité : les outils
La qualité se discute mieux avec des mesures qu'avec des impressions. Plusieurs familles d'outils les fournissent, presque toutes gratuites.
| Outil | Ce qu'il fait | En JavaScript | En PHP |
|---|---|---|---|
| Formateur | Met en forme le code de façon uniforme (indentation, espaces, retours à la ligne) | Prettier | PHP-CS-Fixer |
| Linter | Signale les erreurs probables et les mauvaises pratiques | ESLint | PHP_CodeSniffer |
| Analyse statique | Trouve des bugs sans exécuter le code : types incohérents, méthode inexistante, null possible | TypeScript, ESLint | PHPStan, Psalm |
| Couverture de tests | Mesure la part du code exécutée par les tests | jest --coverage | phpunit --coverage-html coverage (avec Xdebug ou PCOV) |
| Audit de dépendances | Liste les dépendances vulnérables ou obsolètes | npm audit, npm outdated | composer audit, composer outdated |
| Plateforme de qualité | Regroupe les mesures, suit leur évolution, bloque une fusion sous un seuil | SonarQube | SonarQube |
Un linter comme ESLint se configure en quelques lignes. Avec ESLint 10 (et déjà depuis ESLint 9), la configuration tient dans un fichier eslint.config.js :
// eslint.config.js
const js = require('@eslint/js');
const globals = require('globals');
module.exports = [
js.configs.recommended,
{
languageOptions: { sourceType: 'commonjs', globals: globals.node },
rules: {
eqeqeq: 'error', // impose === et !==
'no-console': 'warn', // signale les console.log oubliés
complexity: ['warn', 8], // signale les fonctions trop complexes
},
},
];
Lancé sur la fonction de calcul des remboursements, que le chapitre 8 reprend, il affiche :
$ npx eslint services/remboursement.js
/app/services/remboursement.js
2:1 warning Function 'calc' has a complexity of 9. Maximum allowed is 8 complexity
3:7 error The value assigned to 'r' is not used in subsequent statements no-useless-assignment
4:13 error Expected '===' and instead saw '==' eqeqeq
10:20 error Expected '===' and instead saw '==' eqeqeq
16:20 error Expected '===' and instead saw '==' eqeqeq
17:17 error Expected '===' and instead saw '==' eqeqeq
✖ 6 problems (5 errors, 1 warning)
Chaque ligne donne la position (ligne et colonne), la gravité, le message et le nom de la règle. La règle no-useless-assignment, qui fait partie des règles recommandées depuis ESLint 10, signale que le 0 donné à r à la ligne 3 n'est jamais lu : chaque branche l'écrase. Lancé dans l'éditeur (extension ESLint de VS Code) et dans l'intégration continue, le linter attrape une famille entière de bugs avant même les tests : le = à la place du === du chapitre 4, une variable mal orthographiée, un await dans une fonction qui n'est pas async.
L'analyse statique va plus loin en suivant les types. PHPStan, par exemple, aurait signalé que kmMoyen renvoie null dans un cas et que la vue appelle number_format() dessus :
composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse src --level=6
PHPStan se règle par niveaux, de 0 (vérifications de base) à 10 (le plus strict). On commence bas sur un code existant, on corrige, et on monte d'un niveau à la fois.
La complexité cyclomatique compte le nombre de chemins indépendants dans une fonction : 1, plus 1 pour chaque point de décision (if, else if, case, boucle, opérateur && ou || selon les outils). Une fonction de complexité 2 a deux chemins, donc au moins deux tests à écrire pour les parcourir tous ; une fonction de complexité 25 est presque impossible à tester complètement, et difficile à comprendre. Au-delà de 10, la plupart des équipes considèrent qu'il faut découper.
La couverture de tests indique quelle part du code les tests exécutent :
----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------|---------|----------|---------|---------|-------------------
All files | 78.4 | 61.2 | 83.3 | 78.9 |
notes.js | 100 | 100 | 100 | 100 |
export.js | 0 | 0 | 0 | 0 | 1-48
recap.js | 91.6 | 75 | 100 | 91.6 | 22
----------------|---------|----------|---------|---------|-------------------
Elle te dit ce qui n'est pas testé : ici, l'export est un angle mort complet. Elle ne dit pas que ce qui est couvert est bien testé : une ligne exécutée par un test qui ne vérifie rien compte comme couverte. Un taux de couverture est un indicateur, pas un objectif ; viser 100 % pousse à écrire des tests inutiles, alors que couvrir les calculs et les règles métier rapporte bien plus.
La revue de code
Aucun outil ne remplace un regard humain. La revue de code consiste à faire relire chaque modification par un autre développeur avant de la fusionner, en général dans une pull request. Elle trouve des bugs, mais surtout elle diffuse la connaissance du code dans l'équipe et maintient sa cohérence.
Une grille simple pour relire, ou pour te relire avant de demander une revue :
| Question | Ce que tu vérifies |
|---|---|
| Le changement fait-il ce que demande le ticket, et seulement cela ? | Pas de modification hors sujet glissée au passage |
| Est-il testé ? | Un test pour le comportement nouveau ou corrigé, la suite verte |
| Se lit-il sans explication ? | Des noms clairs, des fonctions courtes, des commentaires sur le pourquoi |
| Les cas limites sont-ils traités ? | Liste vide, null, valeur maximale, erreur de la base |
| Est-il sûr ? | Requêtes préparées, droits vérifiés, aucune donnée sensible dans les journaux |
| Laisse-t-il des traces de débogage ? | Pas de console.log, de var_dump, de code commenté |
| La documentation suit-elle ? | README, documentation de l'API, changelog |
Le cours Git et GitHub montre comment ouvrir et relire une pull request.
Suivre et rembourser la dette
Une dette se gère comme un budget : on la connaît, on la priorise, on la rembourse par petits versements.
La rendre visible. Chaque dette repérée devient un ticket, avec une étiquette (« dette technique »), une description du problème, de son coût et de la solution envisagée. Un commentaire // TODO dans le code est utile seulement s'il pointe vers ce ticket (// TODO(#262) : extraire le calcul kilométrique) ; sinon, il sera encore là dans cinq ans.
La prioriser par les intérêts. Une dette coûte quand on touche au code concerné. Le code mal écrit mais stable, que personne ne modifie jamais, peut attendre. Le code mal écrit et souvent modifié doit passer en premier : c'est là que les intérêts courent. L'historique Git le révèle : git log --format= --name-only | sort | uniq -c | sort -rn | head liste les fichiers les plus souvent modifiés.
La rembourser au fil de l'eau. Deux pratiques fonctionnent mieux qu'un grand chantier de « nettoyage » qui n'arrive jamais :
- la règle du scout : laisser chaque fichier que tu touches un peu plus propre que tu ne l'as trouvé (un nom clarifié, une fonction extraite, un test ajouté), dans un commit séparé de ta correction ;
- un budget régulier : une part de chaque itération réservée à la dette, négociée avec le responsable du produit, en expliquant ce qu'elle fera gagner sur les prochaines évolutions.
Qualité et développement durable
Le référentiel demande que le développement tienne compte des préoccupations de développement durable, et la qualité du code y contribue directement. Une requête qui charge 10 000 lignes pour en afficher 20, une image de 4 Mo affichée en vignette, une page qui interroge l'API toutes les secondes : ces défauts consomment de l'énergie et du matériel pour rien, et ce sont aussi des défauts de performance. Les corriger (pagination, index, images redimensionnées, cache) est de la maintenance, souvent préventive. En France, le référentiel général d'écoconception de services numériques (RGESN) rassemble ces bonnes pratiques.
Exercices
Exercice 1 sur 4 Classer la dette Réussi À revoir
Place chaque situation dans le tableau de Martin Fowler (délibérée ou involontaire, prudente ou imprudente).
- Pour livrer l'export CSV avant la clôture comptable, l'équipe l'écrit sans tests, et crée un ticket pour les ajouter la semaine suivante.
- Un alternant écrit toutes les requêtes SQL dans les contrôleurs, parce qu'il ne connaît pas encore le principe des couches.
- Après un an, l'équipe comprend que les montants auraient dû être stockés en centimes dès le départ.
- « Les tests, on verra plus tard » : la phrase revient à chaque version depuis deux ans, sans ticket ni plan.
Voir la réponseMasquer la réponse de l'exercice 1
- Délibérée et prudente : un choix conscient, limité, avec un plan de remboursement.
- Involontaire et imprudente : faute de connaissances. Elle se réduit par la formation et la revue de code.
- Involontaire et prudente : l'équipe a appris en chemin. Inévitable, à rembourser si les intérêts le justifient.
- Délibérée et imprudente : la plus coûteuse. Sans tests, chaque modification devient un pari.
Tu avais trouvé ?
Exercice 2 sur 4 Complexité cyclomatique Réussi À revoir
function niveauAlerte(total, plafond) {
if (plafond === null) return 'aucune';
if (total > plafond) return 'depassement';
if (total > plafond * 0.9) return 'proche';
return 'normal';
}
- Quelle est la complexité cyclomatique de cette fonction ?
- Combien de tests faut-il au minimum pour parcourir tous ses chemins ? Propose des valeurs pour chacun, avec un plafond de 1 000.
- Quel cas limite ajouterais-tu, et pourquoi ?
Voir la réponseMasquer la réponse de l'exercice 2
- Trois
if, donc 3 + 1 = 4. - Quatre tests, un par chemin :
niveauAlerte(500, null)→'aucune';niveauAlerte(1200, 1000)→'depassement';niveauAlerte(950, 1000)→'proche';niveauAlerte(500, 1000)→'normal'.
- Les égalités :
niveauAlerte(1000, 1000)donne'proche'(1 000 n'est pas strictement supérieur à 1 000, mais l'est à 900), etniveauAlerte(900, 1000)donne'normal'. Est-ce le comportement voulu ? Le test d'égalité oblige à poser la question au métier, et fige la réponse.
Tu avais trouvé ?
Exercice 3 sur 4 Lire un rapport de linter Réussi À revoir
/app/services/export.js
8:9 error 'separateur' is not defined no-undef
14:15 error Expected '===' and instead saw '==' eqeqeq
21:11 error 'ancienFormat' is defined but never used no-unused-vars
30:3 warning Unexpected console statement no-console
Pour chaque message, explique le problème et dis s'il peut provoquer un bug en production.
Voir la réponseMasquer la réponse de l'exercice 3
no-undef:separateurest utilisé sans avoir été déclaré, sans doute une faute de frappe ou une déclaration oubliée. En production, la ligne 8 lèvera uneReferenceErrordès qu'elle s'exécutera : c'est un bug certain.eqeqeq:==compare après conversion de type (0 == ''est vrai,'1' == 1aussi). Bug possible, selon les valeurs comparées ;===supprime le doute.no-unused-vars:ancienFormatest déclaré mais jamais utilisé. Pas de bug, mais du code mort qui trompe le lecteur. À supprimer (Git le garde dans l'historique).no-console: unconsole.logoublié. Pas de bug fonctionnel, mais il pollue les journaux et peut y écrire des données personnelles. À retirer.
Tu avais trouvé ?
Exercice 4 sur 4 Prioriser la dette Réussi À revoir
L'équipe de Sorbier a une demi-journée par semaine pour la dette. Classe ces quatre chantiers par ordre de priorité, en justifiant.
- Le module de génération des PDF, écrit il y a trois ans, illisible, modifié une fois depuis.
- Le calcul des remboursements, plein de
ifimbriqués et de montants en dur, modifié à chaque changement de barème (quatre fois cette année), sans tests. - La bibliothèque d'envoi de mails, signalée par
npm auditavec une faille de gravité élevée. - Les noms de variables en anglais et en français mélangés dans tout le projet.
Voir la réponseMasquer la réponse de l'exercice 4
- En premier, le chantier 3 : une faille connue de gravité élevée est un risque immédiat, et la corriger est souvent rapide (mise à jour de la dépendance).
- Ensuite, le chantier 2 : c'est la dette aux intérêts les plus élevés, un code fragile, sans tests, et modifié souvent. On commence par des tests de caractérisation, puis on refactorise (chapitre 8).
- Au fil de l'eau, le chantier 4 : gênant mais sans risque ; on harmonise les noms quand on touche un fichier, selon la règle du scout, plutôt qu'en une seule fois.
- En dernier, le chantier 1 : illisible mais stable et presque jamais modifié. Ses intérêts sont faibles. On le reprendra le jour où une évolution le touchera.
Tu avais trouvé ?