Génie logiciel · C4 Qualité du code · Chapitre 2 · 5 h
Principes de conception
Séparation des responsabilités ; couplage et cohésion ; éviter la duplication ; dette technique ; refactoring d'un code volontairement mal écrit.
La lisibilité (chapitre 6) agit à l'échelle d'une ligne, d'une fonction. La conception monte d'un cran : comment organiser le code en un tout qui résiste au changement ? Car un logiciel qui dure sera modifié des dizaines de fois (chapitre 1), et une bonne conception est précisément celle qui rend le changement facile plutôt que douloureux.
Ce chapitre présente les principes fondamentaux — séparation des responsabilités, couplage, cohésion, non-duplication — introduit la notion de dette technique, et met en pratique le refactoring sur un code volontairement mal écrit. Ces principes s'appuient naturellement sur les classes de la POO : si votre maquette la propose avant cette UE, vous êtes en terrain connu.
Séparer les responsabilités
Le principe le plus fondamental de la conception : chaque module (fonction, classe) ne devrait avoir qu'une seule responsabilité — une seule raison de changer.
Une fonction qui, à elle seule, lit les données, les calcule, les met en forme et les affiche a quatre raisons de changer : un nouveau format d'entrée, une nouvelle règle de calcul, un nouvel affichage, une nouvelle sortie. Chaque évolution risque de casser les autres. En séparant ces responsabilités — une fonction par tâche —, chacune devient modifiable, testable et réutilisable indépendamment.
C'est le prolongement, à l'échelle de la conception, du « une fonction fait une chose » du chapitre 6. La question à se poser devant tout module : « combien de raisons différentes aurais-je de le modifier ? » Si la réponse est « plusieurs », il faut probablement le découper.
Couplage et cohésion
Deux notions jumelles mesurent la qualité d'un découpage. On les vise en sens opposés.
Le couplage mesure à quel point les modules dépendent les uns des autres. On le veut faible : un module qu'on peut comprendre, modifier ou tester sans toucher aux autres. Un couplage fort — un module qui connaît les détails internes d'un autre — propage chaque changement en cascade : toucher l'un casse l'autre.
La cohésion mesure à quel point les éléments d'un même module vont ensemble. On la veut
forte : une classe Facture qui ne s'occupe que de facturation est cohérente ; une classe
Utilitaires qui rassemble des fonctions sans rapport (dates, réseau, chaînes) ne l'est pas.
Bonne conception = couplage faible entre modules, cohésion forte à l'intérieur de chacun.
L'image : des modules bien séparés (faiblement couplés), chacun rassemblant ce qui va ensemble (fortement cohésif). C'est cette structure qui permet de modifier une partie sans faire trembler tout l'édifice — exactement ce dont un logiciel qui évolue a besoin.
Quiz · 1 question
Que vise-t-on pour le couplage et la cohésion d'un logiciel bien conçu ?
- Couplage fort et cohésion forte, pour que tout soit bien relié — tout relié
- Couplage faible entre modules (ils dépendent peu les uns des autres) et cohésion forte à l'intérieur de chacun (il regroupe des choses qui vont ensemble) — faible entre / forte dedans
- Couplage faible et cohésion faible, pour un maximum de souplesse — tout faible
Réponse : On vise un COUPLAGE FAIBLE entre modules — chacun se comprend, se modifie et se teste sans toucher aux autres, donc un changement ne se propage pas en cascade — et une COHÉSION FORTE à l'intérieur de chacun — il ne rassemble que des choses qui vont ensemble (une classe Facture qui ne fait que de la facturation). Un couplage fort propage les changements ; une cohésion faible (une classe fourre-tout) rend le module illisible et instable. Les deux se visent en sens opposés.
Éviter la duplication : DRY
La duplication de code est l'un des pires défauts de conception, et le plus fréquent. Le principe qui la combat s'appelle DRY — Don't Repeat Yourself : chaque connaissance ne doit exister qu'à un seul endroit dans le code.
Copier-coller un bloc est tentant — c'est rapide. Mais le jour où la logique dupliquée doit changer (une remise qui passe de 10 % à 15 %), il faut la corriger partout — et l'on en oublie toujours une, créant des comportements incohérents et un bug silencieux. Le remède est l'extraction : sortir la logique commune dans une fonction (ou classe) que tout le monde appelle.
Une nuance de bon sens : DRY concerne la duplication de connaissance, pas la ressemblance superficielle. Deux bouts de code qui se ressemblent aujourd'hui par hasard mais évolueront pour des raisons différentes n'ont pas à être fusionnés — les unir créerait un couplage artificiel. On factorise ce qui est vraiment la même règle, destinée à changer ensemble. L'exercice vous fait pratiquer cette extraction.
La dette technique
Voici une métaphore qui éclaire tous les compromis de qualité : la dette technique. Quand on prend un raccourci — copier au lieu de factoriser, bâcler au lieu de concevoir, sauter les tests — on emprunte du temps : on va plus vite maintenant.
Mais comme une dette financière, ce raccourci porte des intérêts : chaque modification future du code mal conçu coûte plus cher, plus longtemps. Tant qu'on ne rembourse pas — en refactorant —, les intérêts s'accumulent, jusqu'à ce que la moindre évolution devienne un cauchemar et que le projet s'enlise.
La dette n'est pas toujours mauvaise : emprunter délibérément pour livrer à temps une démonstration peut être un bon calcul, à condition de le savoir et de rembourser ensuite. Ce qui tue les projets, c'est la dette subie et ignorée — accumulée sans en avoir conscience. Nommer la dette, c'est se donner les moyens de décider quand l'emprunt en vaut la peine, et quand il faut s'arrêter pour rembourser.
Le refactoring
Le refactoring est l'acte de rembourser la dette : améliorer la structure interne du code sans changer son comportement. C'est la définition exacte, et le mot « sans changer son comportement » est tout : refactorer, ce n'est pas réécrire au hasard ni ajouter des fonctionnalités, c'est transformer la forme en préservant le fond.
Extraire une fonction dupliquée, renommer pour clarifier, découper un module trop gros, réduire un couplage : autant de refactorings. On procède par petits pas, en vérifiant après chacun que rien n'a cassé.
D'où le lien indissociable avec le chapitre suivant : on ne refactore jamais sans tests. Les tests sont le filet qui garantit que la transformation a préservé le comportement. Sans eux, chaque refactoring est un pari — on ne sait pas si on a amélioré le code ou introduit un bug. C'est précisément ce que vérifie l'exercice de ce chapitre : les mêmes tests passent avant et après.
Quiz · 1 question
Qu'est-ce que le refactoring, et pourquoi ne le pratique-t-on jamais sans tests ?
- Réécrire le code en ajoutant des fonctionnalités ; les tests ralentissent inutilement — ajouter des fonctions
- Améliorer la structure interne du code SANS changer son comportement ; les tests sont le filet qui garantit qu'on n'a rien cassé en transformant — structure sans comportement + filet de tests
- Corriger les bugs d'un code ; les tests servent après, une fois les bugs corrigés — corriger des bugs
Réponse : Le refactoring améliore la STRUCTURE (lisibilité, duplication, couplage) en préservant STRICTEMENT le comportement — ce n'est ni ajouter des fonctionnalités, ni corriger des bugs. Justement parce que le comportement doit rester identique, on a besoin de tests pour le PROUVER : ils forment le filet qui dit « rien n'a cassé » après chaque petit pas. Sans tests, un refactoring est un pari — on ne peut pas distinguer une amélioration d'une régression introduite. D'où : jamais de refactoring sans tests.
À vous
L'exercice met en pratique le principe le plus rentable du chapitre : DRY. Vous partez d'un code où la même logique de remise est copiée trois fois, et vous l'extrayez en une seule fonction — sans changer le comportement, comme le vérifient les tests.
Vous toucherez du doigt pourquoi la duplication est une dette (le jour où la remise change, trois corrections au lieu d'une), et pourquoi le refactoring exige des tests : ce sont eux qui prouvent que votre transformation a préservé le fond.
Exercice de code
Refactorez un code où la même logique de remise est copiée trois fois : extrayez-la en une seule fonction (DRY), sans changer le comportement — les tests le vérifient. Reliez cela au couplage/cohésion, à la dette technique, et à la raison pour laquelle on ne refactore jamais sans tests.
Point de départ
// Un code qui MARCHE mais qui a mal vieilli : la même logique de remise est
// COPIÉE-COLLÉE trois fois, avec une petite variation. Le jour où la remise
// change, il faut la corriger à trois endroits — et on en oublie toujours un.
function prixLivre(prix, quantite) {
let total = prix * quantite;
if (total > 100) { total = total * 0.9; } // -10 % au-dessus de 100
return total;
}
function prixDVD(prix, quantite) {
let total = prix * quantite;
if (total > 100) { total = total * 0.9; } // -10 % (copié)
return total;
}
function prixJeu(prix, quantite) {
let total = prix * quantite;
if (total > 100) { total = total * 0.9; } // -10 % (encore copié)
return total;
}
// ── À VOUS : refactorer sans changer le comportement ────────────────────────
// 1. Extraire la logique commune dans UNE fonction (DRY : Don't Repeat
// Yourself). Le calcul de remise doit exister à UN seul endroit.
// 2. Réécrire prixLivre/prixDVD/prixJeu pour qu'ils l'utilisent (ou même
// disparaissent au profit d'un seul « prix »).
function prix(prixUnitaire, quantite) {
return 0; // à compléter : total, puis remise via une seule fonction
}
// ── Vérification : MÊMES résultats que les versions d'origine ───────────────
const cas = [ [10, 5], [30, 4], [50, 3], [200, 1], [20, 2] ];
let ok = true;
for (const [p, q] of cas) {
const attendu = prixLivre(p, q); // les 3 originales donnaient pareil
const obtenu = prix(p, q);
const bon = attendu === obtenu;
ok = ok && bon;
console.log((bon ? " ok " : " ✗ ") + "prix(" + p + "," + q + ") = " + attendu + " / " + obtenu);
}
console.log(ok ? "Comportement préservé : c'est un refactoring valide." : "Le résultat a changé !");
Solution
const SEUIL_REMISE = 100;
const TAUX_REMISE = 0.9; // -10 %
// UNE seule définition de la remise : une seule responsabilité, un seul endroit
// à corriger le jour où la règle change.
function appliquerRemise(total) {
return total > SEUIL_REMISE ? total * TAUX_REMISE : total;
}
function prix(prixUnitaire, quantite) {
return appliquerRemise(prixUnitaire * quantite);
}
// prixLivre/prixDVD/prixJeu deviennent inutiles : ils faisaient tous la même
// chose. Un seul « prix » suffit. Comportement strictement identique.
// ── Ce que l'exercice enseigne ──────────────────────────────────────────────
//
// 1. DRY — Don't Repeat Yourself. La logique dupliquée est une bombe à
// retardement : le jour où la remise passe à -15 %, on modifie un endroit
// et on OUBLIE les deux autres -> trois comportements incohérents, un bug
// silencieux. Une logique = UN seul endroit dans le code.
//
// 2. SÉPARATION DES RESPONSABILITÉS : « calculer un total » et « appliquer une
// remise » sont deux responsabilités distinctes. Les isoler (appliquerRemise)
// rend chacune testable et réutilisable seule. Une fonction, une raison de
// changer.
//
// 3. COUPLAGE et COHÉSION : on veut un couplage FAIBLE (les modules dépendent
// peu les uns des autres) et une cohésion FORTE (chaque module regroupe des
// choses qui vont ensemble). appliquerRemise est cohérente (elle ne fait
// qu'une chose) et faiblement couplée (elle ne dépend que de son argument).
//
// 4. REFACTORING = améliorer la STRUCTURE sans changer le COMPORTEMENT. La
// preuve que c'en est un : les tests passent à l'identique avant et après.
// C'est pourquoi on ne refactore JAMAIS sans tests (chapitre 8) — ils sont
// le filet qui garantit qu'on n'a rien cassé.
//
// 5. La DETTE TECHNIQUE : la duplication d'origine était un « emprunt » (copier
// a été rapide). Comme une dette, elle porte des INTÉRÊTS : chaque
// modification future coûte plus cher. Le refactoring, c'est rembourser la
// dette avant qu'elle n'étouffe le projet.
Ce que la suite en fait
Le refactoring de ce chapitre a montré son propre présupposé : il repose sur des tests. C'est l'objet du bloc V, et le cœur pratique de l'UE.
Le chapitre 8 traite les tests — pourquoi « ça marche chez moi » ne suffit pas, et comment écrire des tests qui attrapent les régressions, y compris celles qu'un refactoring pourrait introduire. Le chapitre 9 apprend Git — travailler à plusieurs sans se marcher dessus. Ce sont les deux outils qui transforment les principes des blocs précédents en pratique quotidienne d'équipe.
À retenir
Flashcards · 4 cartes
- Qu'est-ce que la séparation des responsabilités, et à quelle question la relier ?
- Chaque module (fonction, classe) ne devrait avoir qu'UNE responsabilité — une seule raison de changer. Une fonction qui lit, calcule, met en forme et affiche a quatre raisons de changer, chacune risquant de casser les autres. Question à se poser : « combien de raisons différentes aurais-je de modifier ce module ? » Si c'est plusieurs, il faut probablement le découper.
- Que visent le couplage et la cohésion dans une bonne conception ?
- Couplage FAIBLE entre modules (ils dépendent peu les uns des autres : on en modifie un sans toucher aux autres, pas de cascade) et cohésion FORTE à l'intérieur de chacun (il ne regroupe que des choses qui vont ensemble). C'est cette structure — modules bien séparés, chacun rassemblant ce qui va ensemble — qui permet de modifier une partie sans faire trembler tout l'édifice.
- Qu'est-ce que le principe DRY, et quelle nuance faut-il garder ?
- DRY (Don't Repeat Yourself) : chaque connaissance ne doit exister qu'à UN seul endroit. La duplication est une bombe à retardement — le jour où la règle change, on corrige partout et on en oublie une (bug silencieux). On extrait la logique commune dans une fonction. Nuance : DRY vise la duplication de CONNAISSANCE (même règle destinée à changer ensemble), pas la ressemblance superficielle de deux codes qui évolueront séparément.
- Qu'est-ce que la dette technique, et qu'est-ce que le refactoring ?
- La DETTE TECHNIQUE : le coût futur d'un raccourci pris aujourd'hui (copier au lieu de factoriser). Comme une dette, elle porte des intérêts — chaque modification future coûte plus cher — jusqu'au remboursement. Le REFACTORING est ce remboursement : améliorer la structure interne SANS changer le comportement, par petits pas. On ne refactore jamais sans tests : ils sont le filet qui prouve que rien n'a cassé.