Les fonctions
Petites, focalisées, à un seul niveau d'abstraction : l'anatomie d'une fonction propre et la maîtrise de ses arguments.
Les fonctions sont la première ligne d'organisation de tout programme. Ce sont les verbes du langage que nous inventons pour décrire notre système. Et pourtant, ce sont aussi le premier endroit où le désordre s'installe : la fonction de 300 lignes qui « fait tout », celle dont on ne sait plus si elle lit ou écrit, celle qu'on n'ose plus toucher de peur de casser autre chose ailleurs.
Robert C. Martin ne propose pas de théorie abstraite : il a écrit, sur quatre décennies, des fonctions de toutes les tailles — y compris quelques « abominations » de 3 000 lignes. Ce que l'expérience lui a appris tient en quelques règles concrètes que nous allons dérouler ici. Les exemples du livre sont en Java ; nous les traduisons en TypeScript idiomatique, dans un univers e-commerce, paie et géométrie.
Petites. Puis encore plus petites.
La première règle des fonctions est qu'elles doivent être petites. La seconde règle est qu'elles doivent être plus petites que ça.
Dans les années 80, on disait qu'une fonction ne devait pas dépasser la hauteur d'un écran. À l'époque, un écran faisait 24 lignes. Aujourd'hui, avec une police rapetissée sur un grand moniteur, on case 100 lignes ou plus à l'écran — mais cela ne rend pas une fonction de 100 lignes acceptable pour autant. Une fonction ne devrait que rarement dépasser 20 lignes, et souvent bien moins.
Martin raconte avoir vu, chez Kent Beck, un petit programme dont chaque fonction faisait deux, trois ou quatre lignes. Chacune était transparente. Chacune racontait une histoire. Et chacune menait à la suivante dans un ordre limpide. C'est l'objectif.
// ✅ Trois lignes, une seule idée par fonction.
function rendrePageAvecSetups(page: PageData, suite: boolean): string {
if (estPageDeTest(page)) inclureSetupsEtTeardowns(page, suite);
return page.html;
} Blocs et indentation
Si les fonctions doivent être minuscules, alors les blocs à l'intérieur des if, else et while ne devraient faire qu'une seule ligne — et cette ligne devrait probablement être un appel de fonction. Cela garde la fonction englobante courte, mais surtout cela ajoute de la valeur documentaire : la fonction appelée porte un nom descriptif qui explique le bloc mieux qu'un commentaire.
Il en découle un corollaire : le niveau d'indentation d'une fonction ne devrait jamais dépasser un ou deux. Pas de boucle dans une condition dans une autre boucle.
// ❌ Avant : indentation profonde, plusieurs niveaux imbriqués.
function appliquerRemises(panier: Panier): void {
for (const article of panier.articles) {
if (article.eligibleRemise) {
if (article.quantite > 10) {
article.prix = article.prix * 0.9;
}
}
}
}
// ✅ Après : chaque niveau extrait, indentation plate.
function appliquerRemises(panier: Panier): void {
for (const article of panier.articles) appliquerRemise(article);
}
function appliquerRemise(article: Article): void {
if (estEligibleRemiseVolume(article)) article.prix *= 0.9;
}
function estEligibleRemiseVolume(article: Article): boolean {
return article.eligibleRemise && article.quantite > 10;
} Faire une seule chose
Le conseil traverse les décennies sous une forme ou une autre :
Les fonctions devraient faire une seule chose. Elles devraient bien la faire. Elles ne devraient faire que cela.
Tout le problème est de savoir ce qu'« une seule chose » signifie. Reprenons notre fonction rendrePageAvecSetups. On pourrait soutenir qu'elle fait trois choses : déterminer si la page est une page de test, y inclure les setups et teardowns, puis rendre le HTML. Alors, une ou trois ?
La clé est l'abstraction. Ces trois étapes sont toutes un niveau en dessous du nom de la fonction. On peut décrire la fonction par un petit paragraphe « POUR » :
POUR rendre la page avec ses setups et teardowns, on vérifie si c'est une page de test et, si oui, on inclut les setups et teardowns ; dans tous les cas, on rend le HTML.
Si une fonction n'exécute que les étapes situées un cran sous son nom, alors elle fait une seule chose. C'est même la raison d'être des fonctions : décomposer un concept plus large (le nom de la fonction) en une série d'étapes au niveau d'abstraction suivant.
Astuce
Test décisif : essayez d'extraire une autre fonction de votre fonction, avec un nom qui ne soit pas une simple reformulation de son code. Si vous y parvenez, c'est qu'elle faisait plus d'une chose.
Autre symptôme révélateur : une fonction divisée en sections (déclarations, initialisations, traitement…) fait manifestement plus d'une chose. Une fonction qui ne fait qu'une chose ne peut pas être découpée en sections.
Un seul niveau d'abstraction par fonction
Pour s'assurer qu'une fonction ne fait qu'une chose, il faut que toutes ses instructions soient au même niveau d'abstraction. Mélanger les niveaux est toujours déroutant : le lecteur ne sait plus distinguer un concept essentiel d'un simple détail.
// ❌ Avant : trois niveaux d'abstraction mélangés.
function genererRecu(commande: Commande): string {
const total = commande.calculerTotalTTC(); // haut niveau
const ligne = `Total : ${total} EUR`; // niveau moyen
return "<div>" + ligne + "</div>\n"; // bas niveau (HTML brut)
} Comme avec les fenêtres brisées, dès qu'un détail de bas niveau côtoie un concept de haut niveau, d'autres détails s'accumulent peu à peu dans la fonction.
// ✅ Après : chaque niveau dans sa propre fonction.
function genererRecu(commande: Commande): string {
return formaterEnHtml(ligneTotal(commande));
}
function ligneTotal(commande: Commande): string {
return `Total : ${commande.calculerTotalTTC()} EUR`;
}
function formaterEnHtml(contenu: string): string {
return `<div>${contenu}</div>\n`;
} La règle de descente
On veut que le code se lise de haut en bas, comme un récit. Chaque fonction devrait être suivie de celles du niveau d'abstraction immédiatement inférieur, de sorte qu'en parcourant le fichier vers le bas, on descende l'échelle d'abstraction un barreau à la fois. Martin appelle cela la règle de descente (stepdown rule).
Le programme se lit alors comme une suite de paragraphes « POUR », chacun décrivant son niveau et renvoyant aux paragraphes du niveau inférieur :
POUR inclure les setups et teardowns, on inclut les setups, puis le contenu de la page, puis les teardowns. POUR inclure les setups, on inclut le setup de suite si c'est une suite, puis le setup standard. POUR inclure le setup de suite, on cherche la page « SuiteSetUp » dans la hiérarchie parente…
Apprendre à maintenir chaque fonction à un seul niveau d'abstraction est difficile, mais c'est la clé pour garder les fonctions courtes et fidèles au principe « une seule chose ».
Les switch : enfouir, puis polymorphiser
Il est difficile de faire un petit switch. Par nature, un switch fait N choses. On ne peut pas toujours les éviter, mais on peut s'assurer que chacun est enfoui dans une classe de bas niveau et jamais répété, grâce au polymorphisme.
Voici le cas emblématique du livre — un calcul de paie selon le type d'employé :
// ❌ Avant : un switch qui grossira à chaque nouveau type.
function calculerPaie(e: Employe): Money {
switch (e.type) {
case "COMMISSION":
return calculerPaieCommission(e);
case "HORAIRE":
return calculerPaieHoraire(e);
case "SALARIE":
return calculerPaieSalarie(e);
default:
throw new TypeEmployeInvalide(e.type);
}
} Cette fonction cumule les défauts. Elle est grande et grossira encore. Elle fait clairement plus d'une chose. Elle viole le principe de responsabilité unique (SRP) — plusieurs raisons de changer. Elle viole le principe ouvert/fermé (OCP) — elle doit changer à chaque nouveau type. Mais le pire, c'est qu'il existe une infinité d'autres fonctions avec la même structure : estJourDePaie(e), verserPaie(e, montant), etc. Toutes auront le même switch délétère.
La solution : enfouir le switch au sous-sol d'une fabrique abstraite (abstract factory) et ne plus jamais le montrer. La fabrique utilise le switch une seule fois, pour créer la bonne instance ; les fonctions métier sont ensuite dispatchées polymorphiquement via l'interface.
// ✅ Après : le polymorphisme remplace le switch dispersé.
interface Employe {
estJourDePaie(): boolean;
calculerPaie(): Money;
verserPaie(montant: Money): void;
}
interface FabriqueEmploye {
creer(dossier: DossierEmploye): Employe;
}
// L'UNIQUE switch du système, caché dans la fabrique.
class FabriqueEmployeImpl implements FabriqueEmploye {
creer(dossier: DossierEmploye): Employe {
switch (dossier.type) {
case "COMMISSION":
return new EmployeCommission(dossier);
case "HORAIRE":
return new EmployeHoraire(dossier);
case "SALARIE":
return new EmployeSalarie(dossier);
default:
throw new TypeEmployeInvalide(dossier.type);
}
}
} À retenir
Règle générale : un switch est tolérable s'il apparaît une seule fois, sert à créer des objets polymorphes, et reste caché derrière une relation d'héritage que le reste du système ne voit jamais.
Des noms descriptifs
Vous savez que vous travaillez sur du code propre quand chaque routine se révèle être à peu près ce que vous attendiez. — Ward Cunningham
La moitié du chemin vers ce principe consiste à bien nommer les petites fonctions qui ne font qu'une chose. Plus une fonction est petite et focalisée, plus il est facile de lui trouver un nom descriptif.
N'ayez pas peur d'un nom long. Un nom long et descriptif vaut mieux qu'un nom court et énigmatique, et vaut mieux qu'un long commentaire explicatif. N'hésitez pas non plus à passer du temps à essayer plusieurs noms et à relire le code avec chacun en place — les IDE modernes rendent le renommage trivial. Chercher un bon nom débouche souvent sur une restructuration heureuse du code.
Enfin, soyez cohérent. Réutilisez les mêmes verbes et noms à travers le module. La série inclureSetupsEtTeardowns, inclureSetups, inclureSetupDeSuite, inclureSetup raconte une histoire — et vous fait immédiatement vous demander où sont passées les fonctions teardown correspondantes.
Les arguments d'une fonction
Le nombre idéal d'arguments est zéro (niladique). Vient ensuite un (monadique), suivi de près par deux (dyadique). Trois arguments (triadique) devraient être évités quand c'est possible. Plus de trois exige une justification très particulière — et ne devrait de toute façon pas exister.
Pourquoi cette aversion ? D'abord, les arguments sont coûteux conceptuellement. Ils sont à un niveau d'abstraction différent du nom de la fonction et forcent le lecteur à connaître un détail dont il n'a pas besoin à cet instant. Ensuite, ils compliquent les tests : il faut couvrir les combinaisons d'arguments. Zéro argument, c'est trivial ; un, c'est facile ; deux, ça se corse ; au-delà, tester chaque combinaison devient décourageant.
| Arités | Nom | Recommandation |
|---|---|---|
| 0 | Niladique | Idéal. |
| 1 | Monadique | Très bien. |
| 2 | Dyadique | Acceptable, avec précaution. |
| 3 | Triadique | À éviter ; réfléchir à deux fois. |
| 4+ | Polyadique | Proscrit ; emballer dans un objet. |
Les formes monadiques courantes
Il existe deux bonnes raisons de passer un seul argument :
- Poser une question sur l'argument :
fichierExiste("mon-fichier"). - Transformer l'argument et retourner le résultat :
ouvrirFichier(nom)transforme un nom en flux.
Une troisième forme, moins fréquente mais utile, est l'événement : un argument en entrée, aucune sortie, qui modifie l'état du système — par exemple motDePasseEchoueNFois(tentatives). À utiliser avec parcimonie, et en rendant son caractère événementiel évident par le nom.
Évitez les formes monadiques qui ne suivent aucun de ces schémas. En particulier, utiliser un argument de sortie pour une transformation est déroutant : si une fonction transforme son entrée, le résultat doit apparaître dans la valeur de retour.
// ❌ Avant : transformation via un argument de sortie.
function transformer(tampon: Tampon): void { /* ... */ }
// ✅ Après : la transformation est la valeur de retour.
function transformer(entree: Tampon): Tampon { /* ... */ } Les arguments drapeau : à proscrire
Passer un booléen à une fonction est une pratique vraiment déplorable. Cela complique immédiatement la signature et proclame haut et fort que la fonction fait plus d'une chose : une chose si le drapeau est true, une autre s'il est false.
// ❌ Avant : que fait render(true) ? Mystère pour le lecteur.
rendrePage(pageData, true);
// ✅ Après : deux fonctions au nom limpide.
rendrePagePourSuite(pageData);
rendrePagePourTestUnitaire(pageData); Fonctions dyadiques
Une fonction à deux arguments est plus difficile à comprendre qu'une monadique. ecrireChamp(nom) glisse sous l'œil ; ecrireChamp(flux, nom) exige une pause le temps d'apprendre à ignorer le premier paramètre. Or les parties du code qu'on ignore sont celles où se cachent les bugs.
Il existe de bonnes dyades : new Point(0, 0) est parfaitement raisonnable, car les deux arguments sont des composantes ordonnées d'une seule valeur. À l'inverse, flux et nom n'ont ni cohésion ni ordre naturels.
Même une dyade « évidente » comme assertEquals(attendu, obtenu) pose problème : combien de fois a-t-on inversé les deux ? Les dyades ne sont pas le mal absolu, mais elles ont un coût. Profitez des mécanismes disponibles pour les convertir en monades : faire de ecrireChamp une méthode du flux (flux.ecrireChamp(nom)), promouvoir le flux en variable d'instance, ou extraire une classe EcrivainDeChamp qui reçoit le flux dans son constructeur.
Les triades et les objets-arguments
Les fonctions à trois arguments sont nettement plus difficiles : les problèmes d'ordre, de pause et d'oubli sont plus que doublés. Réfléchissez très soigneusement avant d'en créer une.
Quand une fonction semble avoir besoin de plus de deux ou trois arguments, c'est souvent que certains devraient être emballés dans une classe. Comparez :
// ❌ Avant : x et y traînent côte à côte.
function creerCercle(x: number, y: number, rayon: number): Cercle;
// ✅ Après : un concept nommé émerge.
function creerCercle(centre: Point, rayon: number): Cercle; Réduire le nombre d'arguments en créant des objets n'est pas de la triche : quand des variables voyagent toujours ensemble — comme x et y —, elles appartiennent probablement à un concept qui mérite un nom à lui.
Listes d'arguments variadiques
Parfois on veut un nombre variable d'arguments. Si tous sont traités à l'identique, ils équivalent à un seul argument de type liste. Ainsi, une fonction de formatage à arguments variables reste dyadique :
// Équivalent à dyadique : (gabarit, liste).
function formater(gabarit: string, ...args: unknown[]): string; Les mêmes règles s'appliquent : une variadique peut être monade, dyade ou triade, mais pas davantage.
Verbes et mots-clés
Un bon nom encode souvent l'intention et l'ordre des arguments. Pour une monade, visez une belle paire verbe/nom : ecrire(nom) est évocateur, et ecrireChamp(nom) l'est encore plus.
La forme mot-clé va plus loin en encodant le nom des arguments dans le nom de la fonction. C'est le remède au piège de l'ordre :
// ❌ Avant : quel argument est l'attendu ?
assertEquals(attendu, obtenu);
// ✅ Après : l'ordre est dans le nom.
assertAttenduEgalObtenu(attendu, obtenu); Pas d'effets de bord
Les effets de bord sont des mensonges.
Votre fonction promet de faire une chose, mais en fait une autre, cachée : modifier une variable de sa classe, un paramètre, ou un état global. Ces modifications sournoises créent des couplages temporels et des dépendances d'ordre.
Considérez cette fonction apparemment innocente. Voyez-vous le piège ?
// ❌ Avant : checkPassword fait BIEN plus que vérifier.
class ValidateurUtilisateur {
verifierMotDePasse(nom: string, motDePasse: string): boolean {
const user = PasserelleUtilisateur.trouverParNom(nom);
if (user !== Utilisateur.NULL) {
const chiffre = user.phraseChiffreeParMotDePasse();
const phrase = this.crypto.dechiffrer(chiffre, motDePasse);
if (phrase === "Mot de passe valide") {
Session.initialiser(); // <-- effet de bord caché !
return true;
}
}
return false;
}
} L'effet de bord, c'est Session.initialiser(). Le nom dit « vérifier le mot de passe » — il ne dit pas qu'il initialise la session. Un appelant qui croit le nom risque d'effacer les données de session existantes simplement en vérifiant un utilisateur.
Ce couplage temporel signifie que verifierMotDePasse ne peut être appelée qu'à certains moments. Si un couplage temporel est inévitable, rendez-le explicite dans le nom : verifierMotDePasseEtInitialiserSession — ce qui, justement, trahit le principe « une seule chose » et vous pousse à découper.
Les arguments de sortie
Les arguments sont naturellement interprétés comme des entrées. Tomber sur un argument qui est en réalité une sortie provoque un arrêt mental.
// ❌ ajouterPiedDePage(rapport) : est-ce rapport l'entrée ou la sortie ?
ajouterPiedDePage(rapport); Il faut aller lire la signature pour comprendre — et tout ce qui force à vérifier la signature est une rupture cognitive. En orienté objet, le besoin d'arguments de sortie disparaît largement, car this joue ce rôle. Préférez donc :
// ✅ La fonction modifie l'état de son propre objet.
rapport.ajouterPiedDePage(); En général : si une fonction doit modifier un état, qu'elle modifie l'état de l'objet qui la porte.
Séparation commande / requête
Une fonction devrait soit faire quelque chose, soit répondre à quelque chose, mais pas les deux. Soit elle modifie l'état d'un objet, soit elle retourne une information le concernant. Faire les deux sème la confusion.
// ❌ set fait-il l'action, ou interroge-t-il un état précédent ?
if (set("nom", "alice")) { /* ... */ } Du point de vue du lecteur, if (set(...)) est ambigu : demande-t-on si l'attribut valait déjà « alice », ou si on vient de le définir avec succès ? Le mot set se lit tantôt comme un verbe, tantôt comme un adjectif. La vraie solution est de séparer la commande de la requête :
// ✅ Une requête, puis une commande. Aucune ambiguïté.
if (attributExiste("nom")) {
definirAttribut("nom", "alice");
} Préférer les exceptions aux codes d'erreur
Retourner un code d'erreur depuis une commande est une violation subtile de la séparation commande/requête : cela pousse à utiliser des commandes comme prédicats de if, et conduit à des structures profondément imbriquées, car l'appelant doit traiter l'erreur immédiatement.
// ❌ Avant : codes d'erreur => arbre de if imbriqués.
if (supprimerPage(page) === E_OK) {
if (registre.supprimerReference(page.nom) === E_OK) {
if (config.supprimerCle(page.cle()) === E_OK) {
journal.log("page supprimée");
} else {
journal.log("clé de config non supprimée");
}
} else {
journal.log("échec suppression de la référence");
}
} else {
journal.log("échec de la suppression");
} Avec des exceptions, le traitement d'erreur se sépare du chemin nominal et se simplifie :
// ✅ Après : chemin heureux d'un côté, erreurs de l'autre.
try {
supprimerPage(page);
registre.supprimerReference(page.nom);
config.supprimerCle(page.cle());
} catch (e) {
journal.log((e as Error).message);
} Extraire les blocs try/catch
Les blocs try/catch sont laids en soi : ils brouillent la structure et mêlent traitement d'erreur et traitement normal. Mieux vaut extraire leurs corps dans des fonctions dédiées.
// ✅ delete ne parle QUE de gestion d'erreur.
function supprimer(page: Page): void {
try {
supprimerPageEtReferences(page);
} catch (e) {
journaliserErreur(e);
}
}
function supprimerPageEtReferences(page: Page): void {
supprimerPage(page);
registre.supprimerReference(page.nom);
config.supprimerCle(page.cle());
}
function journaliserErreur(e: unknown): void {
journal.log((e as Error).message);
} Note
La gestion d'erreur est une chose. Donc une fonction qui gère les erreurs ne devrait rien faire d'autre. En pratique : si le mot-clé try apparaît, il doit être le tout premier de la fonction, et il ne doit rien y avoir après le bloc catch/finally.
Bonus côté dépendances : un fichier qui définit tous les codes d'erreur (un grand enum Error) devient un aimant à dépendances — le modifier force à recompiler tout ce qui l'importe, ce qui décourage l'ajout de nouveaux codes. Les exceptions, elles, sont de simples dérivées de la classe d'erreur : on en ajoute sans rien casser ailleurs.
Ne vous répétez pas (DRY)
La duplication est peut-être la racine de tous les maux du logiciel. Elle gonfle le code, et impose de modifier chaque copie le jour où l'algorithme change — autant d'occasions d'oublier une copie.
Piège courant
La duplication n'est pas toujours visible. Elle peut être disséminée et entrelacée avec d'autre code, comme un algorithme répété quatre fois pour quatre cas légèrement différents. Traquez-la : factoriser ces quatre copies dans une seule fonction améliore radicalement la lisibilité de tout le module.
Énormément de principes existent pour la combattre : les formes normales des bases de données, l'héritage qui concentre le code dans les classes de base, la programmation structurée ou orientée aspect. Depuis l'invention du sous-programme, l'histoire du génie logiciel est en grande partie une lutte continue contre la duplication.
Programmation structurée
Dijkstra recommandait qu'une fonction n'ait qu'un seul point d'entrée et un seul point de sortie : un seul return, aucun break ni continue dans les boucles, et jamais de goto.
Ces règles apportent un vrai bénéfice dans les grandes fonctions. Mais si vous gardez vos fonctions petites, un return, break ou continue multiple occasionnel ne fait aucun mal — il est parfois même plus expressif que la règle entrée-unique/sortie-unique. Le goto, lui, n'a de sens que dans les grandes fonctions ; on l'évite donc toujours.
Comment écrit-on de telles fonctions ?
Pas du premier coup. Écrire du logiciel, c'est comme toute écriture : on couche d'abord ses idées, maladroitement, puis on les remanie jusqu'à ce qu'elles se lisent bien.
Quand Martin écrit une fonction, elle sort d'abord longue, indentée, avec des listes d'arguments à rallonge, des noms arbitraires et de la duplication. Mais il dispose d'une suite de tests couvrant chacune de ces lignes maladroites. Alors il la malaxe : il extrait des fonctions, renomme, élimine la duplication, raccourcit, réordonne, parfois sort des classes entières — en gardant les tests au vert à chaque étape. Au bout du compte, les fonctions respectent les règles de ce chapitre. Personne ne les écrit ainsi du premier jet.
À retenir
Souvenez-vous du but ultime : les fonctions sont les verbes du langage spécifique à votre domaine, les classes en sont les noms. Les grands programmeurs pensent les systèmes comme des histoires à raconter, pas comme des programmes à écrire.
À retenir
- Petites, puis plus petites encore : visez quelques lignes, indentation 1 ou 2, blocs de
if/whileréduits à un appel. - Une seule chose, un seul niveau d'abstraction : si on peut en extraire une fonction au nom non trivial, c'est qu'elle en faisait trop. Lisez le code de haut en bas (règle de descente).
- Moins d'arguments, c'est mieux : 0 > 1 > 2 ; fuyez 3 et au-delà. Proscrivez les drapeaux booléens ; emballez les arguments groupés dans un objet.
- Pas d'effets de bord, pas d'arguments de sortie : une fonction fait ce que son nom dit, et modifie l'état de son propre objet. Séparez commande et requête.
- Exceptions plutôt que codes d'erreur : isolez le chemin heureux, extrayez les
try/catchdans leurs propres fonctions. - DRY : la duplication est la racine de bien des maux ; factorisez-la sans relâche, tests à l'appui.