Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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éristiqueCe qu'elle mesureSur Sorbier Frais
Aptitude fonctionnelleL'application fait ce qui est demandé, et justeLes totaux sont exacts
Efficacité de performanceTemps de réponse, ressources consomméesLa liste des notes s'affiche en moins d'une seconde
CompatibilitéFonctionner avec d'autres systèmesL'export CSV s'ouvre dans le logiciel comptable
Capacité d'interactionFacilité d'usage, accessibilitéUn salarié saisit une note sans formation
FiabilitéDisponibilité, tolérance aux pannesL'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 logicielUne évolution ne demande pas de tout relire
FlexibilitéFacilité à changer d'environnementL'API peut passer d'un serveur à un conteneur
SûretéAbsence de danger pour les personnes et les biensPeu 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 if imbriqué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 :

ImprudentePrudenteDélibéréeInvolontaire« On n'a pas le temps defaire de la conception. »la plus coûteuse« Il faut livrer maintenant ;on y reviendra ensuite. »un choix défendable, avec un ticket« Une architecture encouches, c'est quoi ? »se réduit par la formation« Maintenant, on sait commenton aurait dû faire. »inévitable dans un projet qui vit
Les quatre formes de dette technique, d'après Martin Fowler
  • 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.

OutilCe qu'il faitEn JavaScriptEn PHP
FormateurMet en forme le code de façon uniforme (indentation, espaces, retours à la ligne)PrettierPHP-CS-Fixer
LinterSignale les erreurs probables et les mauvaises pratiquesESLintPHP_CodeSniffer
Analyse statiqueTrouve des bugs sans exécuter le code : types incohérents, méthode inexistante, null possibleTypeScript, ESLintPHPStan, Psalm
Couverture de testsMesure la part du code exécutée par les testsjest --coveragephpunit --coverage-html coverage (avec Xdebug ou PCOV)
Audit de dépendancesListe les dépendances vulnérables ou obsolètesnpm audit, npm outdatedcomposer audit, composer outdated
Plateforme de qualitéRegroupe les mesures, suit leur évolution, bloque une fusion sous un seuilSonarQubeSonarQube

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 :

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 :

text
$ 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 :

bash
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 :

text
----------------|---------|----------|---------|---------|-------------------
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 :

QuestionCe 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).

  1. 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.
  2. Un alternant écrit toutes les requêtes SQL dans les contrôleurs, parce qu'il ne connaît pas encore le principe des couches.
  3. Après un an, l'équipe comprend que les montants auraient dû être stockés en centimes dès le départ.
  4. « 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
  1. Délibérée et prudente : un choix conscient, limité, avec un plan de remboursement.
  2. Involontaire et imprudente : faute de connaissances. Elle se réduit par la formation et la revue de code.
  3. Involontaire et prudente : l'équipe a appris en chemin. Inévitable, à rembourser si les intérêts le justifient.
  4. 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

js
function niveauAlerte(total, plafond) {
  if (plafond === null) return 'aucune';
  if (total > plafond) return 'depassement';
  if (total > plafond * 0.9) return 'proche';
  return 'normal';
}
  1. Quelle est la complexité cyclomatique de cette fonction ?
  2. Combien de tests faut-il au minimum pour parcourir tous ses chemins ? Propose des valeurs pour chacun, avec un plafond de 1 000.
  3. Quel cas limite ajouterais-tu, et pourquoi ?
Voir la réponseMasquer la réponse de l'exercice 2
  1. Trois if, donc 3 + 1 = 4.
  2. Quatre tests, un par chemin :
    • niveauAlerte(500, null) → 'aucune' ;
    • niveauAlerte(1200, 1000) → 'depassement' ;
    • niveauAlerte(950, 1000) → 'proche' ;
    • niveauAlerte(500, 1000) → 'normal'.
  3. Les égalités : niveauAlerte(1000, 1000) donne 'proche' (1 000 n'est pas strictement supérieur à 1 000, mais l'est à 900), et niveauAlerte(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

text
/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
  1. no-undef : separateur est 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 une ReferenceError dès qu'elle s'exécutera : c'est un bug certain.
  2. eqeqeq : == compare après conversion de type (0 == '' est vrai, '1' == 1 aussi). Bug possible, selon les valeurs comparées ; === supprime le doute.
  3. no-unused-vars : ancienFormat est déclaré mais jamais utilisé. Pas de bug, mais du code mort qui trompe le lecteur. À supprimer (Git le garde dans l'historique).
  4. no-console : un console.log oublié. 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.

  1. Le module de génération des PDF, écrit il y a trois ans, illisible, modifié une fois depuis.
  2. Le calcul des remboursements, plein de if imbriqués et de montants en dur, modifié à chaque changement de barème (quatre fois cette année), sans tests.
  3. La bibliothèque d'envoi de mails, signalée par npm audit avec une faille de gravité élevée.
  4. 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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Refactoring : repérer les code smells et améliorer le codeRevoir le sommaire du cours