La moelle de dizaines de livres techniques, tirée en un seul fil : du courant dans le silicium jusqu'au jugement qu'aucune IA ne remplace. Chaque idée renvoie à sa fiche complète.
On apprend à coder par morceaux : un langage cette année, un pattern le mois suivant, une astuce de perf un jeudi soir. Le savoir s'empile, mais le plan d'ensemble reste flou, et on finit par prendre des décisions sans trop voir d'où elles viennent. Cette page tente l'inverse : tracer une seule carte mentale du métier, qui va du courant dans le silicium jusqu'au jugement humain qu'aucune IA ne remplace. Pas une liste de résumés, mais le fil unique qui les traverse tous. Chaque chapitre est la conséquence du précédent ; chaque idée est racontée une seule fois, à sa place logique, en fondant ensemble tous les livres qui l'enseignent. Lisez de haut en bas, et le métier s'assemble.
La promesse est simple. Que vous débutiez ou que vous codiez depuis des années, vous repartez avec une carte à plusieurs étages qui répond aux questions que vous vous posez vraiment : non seulement quoi faire, mais pourquoi, où, quand et comment le faire.
1
La machine ne comprend que des nombres
le coût
La machine ne pense pas : des nombres entrent, des nombres sortent. Un texte, une image, une condition, tout devient des nombres, et chaque calcul a un coût. La même tâche peut être instantanée ou ramer pendant des secondes, selon la façon dont vous l'écrivez. Ce chapitre vous apprend à voir ce coût avant de le payer. Quatre marches : la plus petite case (le bit), les étages de la mémoire, l'unité de mesure du coût (le Big O), et le traducteur qui s'occupe du reste.
1.1 Tout est nombre
Petit avertissement : c'est le passage le plus bas-niveau du livre, et c'est voulu. Ne cherchez pas à retenir les bits par cœur, visez l'intuition : comprendre pourquoi la machine ne sait stocker que des 0 et des 1. Le socle posé, tout le reste du livre en découle.
Un bit, c'est une case qui vaut 0 ou 1, comme un interrupteur éteint ou allumé : la seule chose que la machine sait physiquement retenir. On les groupe par huit (un octet : huit cases à deux choix chacune, soit 2×2×…×2 = 256 combinaisons, de 0 à 255), et la mémoire n'est qu'une immense rangée de ces octets, chacun repéré par un numéro, son adresse.
Tout s'y ramène : une lettre tient dans un octet (l'ASCII, une convention partagée par toutes les machines, fixe que 'A' vaut 65), un mot en occupe quelques-uns, une image des millions, car chaque pixel est trois nombres (rouge, vert, bleu). Les millions d'octets d'une photo n'ont rien de magique : juste énormément de petites cases de 0 et de 1.
Pourquoi deux valeurs seulement ? Ça paraît primitif. Parce que le monde physique est bruité. Une tension dans un fil dérive avec la chaleur, les parasites, l'âge des composants : distinguer dix niveaux (un par chiffre décimal) demanderait une précision intenable. Deux états, eux, sont sans ambiguïté, « éteint » ou « allumé » ; il faut un bruit énorme pour confondre un 0 et un 1.
Le bit est donc le minimum d'information qui survit au bruit, porté par le composant le plus simple et le moins cher à produire en milliards d'exemplaires : l'interrupteur. Et comme deux états collent à vrai/faux, toute la logique et l'arithmétique se posent par-dessus. Loin d'être simpliste, c'est le compromis le plus robuste qu'on connaisse. On a d'ailleurs essayé autre chose : l'ENIAC de 1945 comptait en décimal, le soviétique Setun de 1958 en ternaire ; le binaire a gagné sur la fiabilité.
Reste à savoir comment une suite de chiffres devient un nombre. Tout tient dans la position. Partons de la base 10, celle qu'on connaît. 234, ce n'est pas « 2, 3, 4 » collés : chaque colonne porte un poids, les unités, les dizaines, les centaines.
Après la virgule, on continue vers le bas : les dixièmes (1/10), les centièmes (1/100)… 0.1 signifie donc « 1 dans la colonne des dixièmes ». Ce n'est pas le point qui fait la valeur, c'est la colonne.
Le binaire suit exactement la même règle, mais avec deux chiffres seulement, et chaque colonne vaut une puissance de 2 : 1, 2, 4, 8, 16… Ainsi 101, c'est 4 + 0 + 1 = 5.
Chaque colonne vaut une puissance de 2 ; on additionne celles dont le bit est à 1.Le mot « Hi! » en mémoire : une lettre par octet. On lit le nombre en additionnant les colonnes dont le bit vaut 1.
Un entier est ainsi une somme exacte de puissances de 2 : il se range toujours parfaitement. Après la virgule, les colonnes binaires deviennent des fractions : 1/2, 1/4, 1/8, 1/16… Un nombre à virgule doit s'écrire comme une somme de celles-là.
Et 0.1 n'y arrive pas. En binaire, une fraction ne tombe juste que si son dénominateur (le bas de la fraction) est une puissance de 2 : 1/2, 1/4, 1/8… Or 0.1 = 1/10, et 10 n'en est pas une. Il cache un facteur 5 (10 = 2 × 5), et 5 n'apparaît jamais quand on double : 2, 4, 8, 16… Du coup, aucune somme finie de 1/2, 1/4, 1/8 ne fait exactement 0.1, et l'écriture part en boucle infinie (0.0001100110011…). Exactement comme 1/3 = 0.333… ne tombe jamais juste en base 10.
Comment la ranger, alors ? Pas avec une virgule à position fixe, mais en notation scientifique binaire : comme on écrit 6,02 × 10²³ en base 10, la machine écrit le nombre comme une mantisse (les chiffres significatifs) fois 2 à un certain exposant (qui dit où tombe la virgule). C'est le sens de « virgule flottante » : l'exposant fait flotter la virgule, ce qui permet de coder l'immense comme l'infime.
Les bits exacts de 0.1, pour les curieux (facultatif)Les bits réels de 0.1 : signe 0 (positif), exposant -4, mantisse qui répète 1001… puis arrondit le dernier groupe en 1010.
Pour 0.1, lisons les trois morceaux. Le signe d'abord : 0, car 0.1 est positif (1 = négatif). La mantisse ensuite : on reprend 0.0001100110011… et on glisse la virgule jusqu'au premier 1, ce qui donne 1,100110011… × 2⁻⁴, dont les chiffres répètent 1001 sans fin. Un nombre ainsi normalisé commence toujours par « 1, », donc ce 1 de tête n'est jamais stocké : un bit gratuit. L'exposant enfin, -4. Les 11 bits qui le stockent ne savent coder que des nombres positifs (0 à 2047), or -4 est négatif. La parade : on lui ajoute un décalage fixe de 1023, ce qui le rend toujours positif. La machine range donc −4 + 1023 = 1019, écrit en binaire 01111111011 (et resoustrait 1023 à la relecture pour retrouver -4).
Mais la mantisse est finie : 52 bits. On coupe le motif infini, et le dernier groupe est arrondi vers le haut (…1001 → …1010). C'est ça, la miette : 0.1 stocké vaut un cheveu de trop. 0.2 subit le même sort, les miettes s'ajoutent, et le total manque 0.3 :
// ce que la machine stocke vraiment (la « miette ») :
0.1 → 0.1000000000000000055… // un peu trop grand
0.2 → 0.2000000000000000111… // un peu trop grand
0.3 → 0.2999999999999999888… // un peu trop petit
0.1 + 0.2 → 0.3000000000000000444… // dépasse le 0.3 stocké
0.1 + 0.2 === 0.3 // false → 0.30000000000000004
0.1 + 0.3 === 0.4 // true… mais par chance !
0.1 + 0.7 === 0.8 // false, comme 0.1 + 0.2
Tout est là : 0.1 et 0.2 penchent un peu trop haut, 0.3 un peu trop bas. La somme des deux dépasse le 0.3 rangé en mémoire ; les deux ne sont pas le même nombre, donc l'égalité est fausse.
Et ce ratage n'a rien d'unique. L'égalité entre deux flottants est une loterie : selon que les miettes s'ajoutent ou se compensent, l'arrondi final tombe juste ou rate, sans qu'on puisse le deviner. 0.1 + 0.3 donne bien 0.4, mais par chance ; 0.1 + 0.7, lui, manque 0.8.
La clé tient en une phrase : un entier est exact, un flottant ne l'est pas. D'où deux réflexes : on ne compare jamais deux flottants avec ==, on teste plutôt s'ils sont assez proches (leur écart sous un petit seuil de tolérance) ; et un montant d'argent se range en centimes, des entiers. Le calcul redevient fiable, et toute une classe de bugs d'arrondi disparaît.
Jusqu'ici, la mémoire n'était qu'une rangée d'octets uniforme. En vrai, elle a des étages. Pourquoi ? Parce que le processeur (la puce qui exécute vos instructions une par une) ne calcule vite que sur des données tout près de lui. Le plus près, ce sont ses registres, une poignée de cases au cœur de la puce : les seules où il calcule. Tout le reste vit plus loin, par paliers : le cache (une réserve rapide collée à la puce, en L1, L2, L3), puis la RAM, puis le disque. Chaque palier qu'on descend est environ dix fois plus grand, mais dix fois plus lent. Sauf le dernier saut : entre la RAM et le disque, ce n'est plus un palier, c'est un gouffre (×100 000).
La pyramide mémoire : en haut, petit et quasi instantané ; en bas, immense et lent. Une donnée qu'on relit souvent doit rester le plus haut possible.
Les ordres de grandeur donnent le vertige : registre quasi instantané, cache ≈ 1 ns, RAM ≈ 100 ns, disque ≈ 10 ms. Pour saisir l'écart, imaginez qu'une lecture en cache prenne 1 seconde : la RAM répondrait alors en ~1 min 40, et le disque dur… en ~4 mois. Une donnée qu'on relit souvent doit donc rester le plus haut possible.
Autre réflexe : la machine ne va jamais chercher un seul octet, elle tire toute une ligne de cache (environ 64 octets) d'un coup. Lire des cases voisines est donc gratuit ; sauter partout coûte cher.
Le cas d'école, c'est le tableau 2D. La mémoire, elle, reste une seule longue rangée d'octets. Un tableau à deux dimensions est donc aplati dans cette rangée : ses lignes posées bout à bout, l'une après l'autre. Résultat : a[i][j] n'est pas un double accès, mais une seule position calculée, i × largeur + j.
De là, deux parcours aux coûts opposés. Parcourir une ligne (j avance), c'est aller de case en case voisine : chacune est déjà dans les 64 octets ramenés par le cache, donc gratuit. Parcourir une colonne (i avance), c'est sauter une largeur entière de tableau à chaque pas : on sort du cache à tous les coups et il faut recharger, donc lent.
(Ça suppose un vrai tableau contigu, comme en C, Go ou NumPy. Un tableau de tableaux (le [[…]] de JS, le int[][] de Java) range chaque ligne ailleurs, allouée à part : là, la contiguïté et le gain de cache tombent.)
Le cadre noir = le lecteur ; la zone colorée = la ligne de cache chargée. En ligne, le lecteur reste dedans (1 chargement) ; en colonne, il en sort à chaque pas et force un rechargement (3).
Même tableau, même calcul, juste l'ordre des deux boucles qui change :
// même logique, deux ordres de boucles sur un tableau 2Dfor (i...) for (j...) a[i][j] // ✓ contigu : la ligne de cache resertfor (j...) for (i...) a[i][j] // ✗ saute une ligne à chaque fois → jusqu'à 10× plus lent
Côté dev, c'est le piège de tous les jours : « je charge tout en mémoire ». Ramener 100 000 lignes d'une base pour n'en afficher que dix, c'est gonfler les données bien au-delà du cache : chaque accès repart alors les chercher loin, en RAM ou sur le disque. La performance s'écroule, pour la même raison de fond que le parcours en colonne : déplacer des données coûte plus cher que calculer.
Écrire du code qui tourne, c'est bien ; savoir s'il tiendra quand la base passe de cent à dix millions de lignes, c'est mieux. Il faut pour ça une unité de mesure : le Big O. Il dit comment le nombre d'étapes grandit avec la taille de l'entrée, notée n, en ignorant les constantes (la machine, le langage). Quatre classes reviennent partout :
O(1) : temps constant, quelle que soit la taille (le plus rapide) ;
O(log n) : la recherche binaire coupe le tas en deux à chaque étape ;
O(n) : un seul passage sur les données ;
O(n²) : tous les couples deux à deux, ça explose vite (le pire ici).
Quand l'entrée grandit, c'est la classe qui décide tout : O(n²) explose là où O(1) ne bouge pas. Aucune machine ne rattrape ça.
Le O(log n), lui, paraît abstrait jusqu'au premier exemple : trouver un nom dans un annuaire d'un milliard d'entrées triées ne coûte qu'une trentaine d'étapes, parce que couper un milliard en deux, trente fois de suite, tombe à 1. C'est pour ça que sa courbe s'aplatit : doubler les données n'ajoute qu'une étape.
Le O(1) est l'idée la plus rentable du quotidien : une table de hachage (le dict Python, l'objet JS, l'array associatif PHP) retrouve une valeur par sa clé en temps constant, même sur un million d'entrées. C'est pourquoi on ne fait jamais if (nom in liste_de_1000) (O(n) : au pire, on parcourt toute la liste) mais if (dictionnaire[nom]) (O(1) : un seul accès direct).
L'écart n'a rien de théorique. L'exemple le plus parlant : un ordinateur des années 1970, des milliers de fois plus lent que le vôtre, mais qui utilise un bon algorithme (O(n)), bat une machine moderne ultra-rapide qui en utilise un mauvais (O(n³)), dès que les données grossissent. La leçon : aucune puissance de calcul ne rattrape un mauvais algorithme.
D'où une habitude qui paie : avant d'écrire une ligne, estimez à la louche le nombre d'opérations. Sur dix millions d'éléments, un seul passage (O(n)) fait dix millions d'opérations, soit une fraction de seconde. Les comparer deux à deux (O(n²)) en fait dix millions × dix millions = cent mille milliards, des heures de calcul. Dix millions de fois plus : ce petit calcul, fait en trente secondes, vous dit lequel est jouable avant même de coder.
La même logique guide le choix de la structure de données. Un tableau range ses éléments côte à côte : atteindre le n-ième est instantané (on calcule son adresse), mais insérer au milieu oblige à décaler tous les suivants d'une case. Une liste chaînée relie chaque élément au suivant par un pointeur : insérer ne coûte que de rebrancher deux liens, mais pour trouver le n-ième il faut suivre la chaîne depuis le début. Aucun n'est « meilleur » : on choisit selon ce qu'on fait le plus, lire ou insérer.
Insérer au milieu : le tableau décale tout ce qui suit ; la liste ne rebranche que 2 liens. Pour atteindre directement le n-ième, c'est l'inverse (le tableau gagne).
On code aujourd'hui en langage clair (PHP, JavaScript, Python…), loin du langage brut de la machine. Entre les deux, un traducteur : le compilateur, qui transforme tout le code en instructions pour la puce avant l'exécution, ou l'interpréteur, qui traduit au fil de l'exécution (PHP, Python ; JavaScript mélange les deux). Les leçons qui suivent valent pour les deux. Connaître un peu ce traducteur, et la machine qu'il vise, reste payant pour deux raisons.
D'abord, ça aide à flairer un coût caché derrière une ligne banale. Le cas d'école : recalculer la longueur d'un texte à chaque tour d'une boucle, alors qu'elle ne change pas. La machine la recompte en entier, des milliers de fois pour rien ; il suffit de la calculer une fois, avant la boucle.
for (let i = 0; i < longueur(texte); i++) { … } // ✗ recomptée à CHAQUE tourconst n = longueur(texte); // ✓ comptée une foisfor (let i = 0; i < n; i++) { … }
Ensuite, ça dit ce que le compilateur fait pour vous, et ce qu'il ne fera jamais. Il optimise les petits détails tout seul : il calcule par exemple 3 × 4 une fois pour toutes, au lieu de le refaire à chaque exécution. Mais il ne touchera jamais à vos grands choix : il ne transformera pas une recherche lente en recherche rapide. L'algorithme reste votre travail ; la machine ne fait que polir ce que vous lui donnez.
Le bas niveau réserve aussi des surprises. Pour aller vite, le processeur parie à l'avance sur le résultat de chaque test if (c'est la « prédiction de branche ») ; quand il se trompe trop souvent, il perd du temps à revenir en arrière. Conséquence absurde mais bien réelle : parcourir un tableau trié peut être plusieurs fois plus rapide que le même parcours sur le même tableau en désordre, parce que les tests deviennent prévisibles.
Or ça, personne ne le devinerait, et des effets cachés comme celui-ci, il y en a beaucoup. La leçon : sur la performance, l'intuition se trompe souvent ; le seul moyen fiable de savoir si un code est rapide, c'est de le mesurer (le chronométrer pour de vrai), jamais de le deviner.
La machine a un coût et ne parle que nombres. Pour exprimer notre intention par-dessus, il nous faut un intermédiaire : le langage de programmation. Et il ne se contente pas de traduire la pensée en instructions, il devient un outil pour penser, des mots qui décident ce qu'on est capable de concevoir.
2
Le langage, outil de pensée
exprimer
La machine ne parle que nombres ; le langage est la couche qui traduit notre intention en instructions. Mais ce n'est pas un simple traducteur : son modèle mental décide de ce qu'on peut penser facilement. Six idées le montrent, de la plus concrète (ce que contient vraiment une variable, pièges compris) à la plus profonde : nommer une chose, c'est étendre ce qu'on peut penser.
2.1 Valeurs et références : ce que la variable contient vraiment
Une variable contient-elle la donnée elle-même, ou seulement son adresse ? Toute la suite en découle. Pour un nombre ou un booléen, la variable est la donnée : l'assigner à une autre la recopie, et les deux vivent séparément.
Mais pour un objet, une liste ou un dictionnaire, la variable ne tient qu'une poignée (un pointeur, c'est-à-dire l'adresse mémoire du chapitre 1) vers la donnée. L'assigner copie la poignée, pas la donnée : on se retrouve avec deux noms pour une seule donnée.
En haut, la valeur se recopie (cases indépendantes) ; en bas, a et b partagent la même donnée, donc modifier par b se voit par a.
const a = [1, 2, 3];
const b = a; // b partage le MÊME tableau de fond
b[0] = 9; // a[0] vaut 9 aussi : une seule donnée derrière deux noms
En PHP, un objet passé à une fonction n'est pas recopié : la fonction modifie l'original. Confondre les deux est une source inépuisable de bugs « j'ai changé une copie et l'original a bougé ». Distinguer les deux n'est pas une astuce de syntaxe : c'est un modèle mental que le langage installe en vous, et c'est lui qui vous fait voir le bug venir.
Et les chaînes de caractères, valeur ou référence ?
Selon le langage, soit la chaîne se copie comme un nombre (PHP), soit elle est immuable : on ne la modifie jamais, on en fabrique une nouvelle (JavaScript, Python). Dans les deux cas le piège disparaît : impossible de « changer la copie et abîmer l'original », elle se comporte comme une valeur.
Vous connaissez les types par leurs noms : int, string, boolean. Mais un type n'est pas qu'une étiquette : c'est l'ensemble des valeurs qu'une variable peut prendre.
le type boolean ne contient que deux valeurs : vrai et faux ;
le type 0 | 1 | 2 (un nombre forcément 0, 1 ou 2) en contient trois ;
le type string, une infinité.
Sous cet angle, combiner deux types revient à opérer sur des ensembles : l'union (A | B) réunit toutes les valeurs, l'intersection (A & B) ne garde que les communes, utile pour exiger qu'un objet honore deux contrats à la fois.
Un type, c'est l'ensemble des valeurs permises. Combiner deux types = les unir (tout) ou les intersecter (le chevauchement).
Cette vision explique aussi le typage structurel : un type est jugé sur ce qu'il a, pas sur son nom. TypeScript le fait sur la forme d'un objet (ses propriétés) ; Go, sur ses interfaces (un type la satisfait dès qu'il en a les méthodes, sans jamais écrire implements).
interface Point { x: number; y: number }
function longueur(p: Point) {…} // longueur attend un Point : un x et un y
longueur({ x: 3, y: 4, z: 5 }) // ✓ ok : la forme {x, y} y est, le z en trop est ignoré
C'est l'inverse du typage nominal (Java, PHP), où l'objet doit déclarer explicitement implements Point pour être accepté. Ici, la forme suffit.
Reste l'idée la plus rentable d'Effective TypeScript, qui tient en deux mots opposés :
let a: any = JSON.parse(s); a.foo.bar // ✗ "fais-moi confiance" : 0 vérif, ça plante à l'exécutionlet b: unknown = JSON.parse(s); b.foo // ✓ erreur de compil : prouve d'abord ce que c'est
any éteint le compilateur et contamine en silence tout ce qu'il touche ; unknown dit « je ne sais pas encore, je vérifierai avant d'agir ». Choisir unknown, c'est garder le filet.
Dans les langages modernes, une fonction est une valeur comme une autre : on la range dans une variable, on la passe en argument, on la renvoie. Cette seule idée débloque trois outils. Ce sont en réalité trois façons de penser que le langage vous ouvre, magiques tant qu'on n'en a pas vu le mécanisme :
la closure : une fonction qui se souvient des variables de l'endroit où elle est née (un compteur qui garde son total privé d'un appel à l'autre) ;
le décorateur : une fonction qui en emballe une autre pour lui ajouter un comportement sans toucher à son code (chronométrer, mettre en cache, vérifier des droits, en une ligne) ;
le générateur (yield) : une fonction qui produit ses valeurs une à une, à la demande, au lieu de tout calculer d'avance (parcourir un fichier énorme sans le charger en entier).
La plus surprenante des trois, c'est la closure. Un exemple en Python :
def compteur():
n = 0
def inc():
nonlocal n; n += 1; return n # inc se SOUVIENT de n (closure)return inc
inc emporte son n : à chaque appel il l'incrémente et s'en souvient, d'où 1, 2, 3… C'est ça, une closure.
Ce mécanisme est partout : JavaScript et Go capturent la variable tout seuls ; PHP demande de la déclarer explicitement, avec use (&$n). Surtout, c'est plus malin qu'un static (une variable qui survit d'un appel de la fonction au suivant) :
un static garde un seul état, partagé par toute la fonction ;
chaque appel de compteur() fabrique un n privé tout neuf : autant de compteurs indépendants qu'on veut (les inc() issus du même appel, eux, partagent le leur).
Au fond, une closure, c'est une machine à fabriquer des états privés.
Le décorateur en découle directement. Le grand classique : chronométrer une fonction sans toucher à son code :
def chrono(f): # prend une fonction, en renvoie une autredef emballage(*a):
t = time(); r = f(*a); print(time() - t) # exécute f et mesure son tempsreturn r
return emballage
@chrono # @ : emballe lent(), qui est maintenant chronométrédef lent(): ...
Et le générateur, le plus contre-intuitif, évite le piège « je charge tout en mémoire » du chapitre 1 en livrant ses valeurs au compte-gouttes, à la demande :
def lignes(fichier):
for ligne in fichier:
yield ligne # rend une ligne, se met en pause, reprend à la suivante# → on lit un fichier de 50 Go sans jamais le charger entier en mémoire
Le générateur produit une ligne, se met en pause en gardant où il en est, puis reprend à la suivante. Les valeurs sortent au compte-gouttes, jamais toutes d'un coup.
Fluent Python pousse l'idée encore plus loin : pas seulement les fonctions, tout objet « parle » le langage natif dès qu'il implémente les bonnes méthodes spéciales. Donnez à votre classe un __len__, et len(mon_objet) marche ; un __iter__, et for x in mon_objet marche, sans hériter de quoi que ce soit. C'est le data model : on ne configure pas Python, on s'y branche.
2.4 La récursivité : résoudre un problème avec une version plus petite de lui-même
Une fonction qui s'appelle elle-même ? On croit d'abord à une boucle infinie, mais non. Tout tient en deux temps :
le cas d'arrêt : minuscule, résolu directement, sans relancer d'appel ;
le cas général : on ramène le problème à une version plus petite de lui-même.
Comme chaque appel rétrécit le problème, on retombe toujours sur le cas d'arrêt. Sans lui, c'est deux miroirs face à face : ça ne s'arrête jamais.
Le secret pour l'écrire sans se nouer le cerveau : régler le cas d'arrêt, puis faire confiance à l'appel plus petit en le supposant déjà résolu. On ne déroule pas toute la cascade dans sa tête.
def fact(n):
if n <= 1: return 1 # cas d'arrêtreturn n * fact(n - 1) # réduit à un problème plus petit# fact(3) = 3 × fact(2) = 3 × 2 × fact(1) = 6
fact(3) appelle fact(2) qui appelle fact(1) : on descend jusqu'au cas d'arrêt, puis chaque appel rend son résultat en remontant : 1, puis 2, puis 6.
Sous le capot, chaque appel attend le résultat du suivant : ils s'empilent (la pile d'appels), puis se dépilent en remontant les résultats. D'où une limite bien réelle : une récursion trop profonde fait déborder la pile (le fameux stack overflow).
C'est l'outil naturel pour tout ce qui est arborescent (en forme d'arbre : une branche qui se redivise en branches plus petites) :
parcourir un dossier et ses sous-dossiers ;
un arbre HTML, le DOM (la page vue comme des balises emboîtées) ;
un JSON imbriqué.
Le code épouse alors la forme des données et devient bien plus court qu'avec des boucles.
Certains langages acceptent presque tout, et c'est un piège qui se referme en silence. JavaScript convertit tout seul les types entre eux (texte, nombre, booléen) : c'est la coercition. Selon le contexte, le même + additionne ou colle bout à bout.
0 == false // true (== convertit false en 0, puis compare)
0 === false // false (=== compare sans convertir : 0 n'est pas false)"5" - 1 // 4 (- n'a aucun sens sur du texte → "5" devient 5)"5" + 1 // "51" (+ voit une chaîne → 1 devient "1", puis collés)
La règle d'or tient à un caractère : toujours ===, jamais ==. Et c'est précisément cette permissivité que TypeScript, le filet de 2.2, est venu corseter. Connaître la permissivité d'un langage, c'est connaître ses bugs avant de les écrire.
Le vrai pouvoir d'un langage, c'est de nommer des abstractions. Une fois qu'on peut dire « une liste », « une interface », « une promesse », on raisonne dessus sans repenser à la plomberie en dessous. Prenons deux de ces noms, dans deux langages.
Premier nom : l'interface. En Go, un type l'implémente sans jamais le déclarer : il suffit qu'il ait les bonnes méthodes. C'est le typage structurel de 2.2, appliqué aux interfaces : on juge le type sur ce qu'il sait faire, jamais sur son nom.
Le consommateur dit « il me faut quelque chose qui sait faire X » sans connaître le type concret derrière ; le fournisseur, lui, n'a rien à déclarer. Ce seul nom suffit à découpler celui qui utilise de celui qui fournit : c'est la graine de toute l'architecture du chapitre 4.
Deuxième nom : la promesse, celle qui fait mal à tout débutant JavaScript. Certaines opérations prennent du temps, par exemple aller chercher des données sur un serveur. JavaScript ne se fige pas à attendre : il continue, et vous rappelle quand le résultat arrive (c'est l'asynchrone).
Avant, on lui passait pour ça une fonction, un callback : « quand tu auras fini, exécute ceci ». Mais dès qu'une requête a besoin du résultat de la précédente, on emboîte un callback dans un callback dans un callback : le code part vers la droite et devient illisible. C'est le fameux « callback hell ».
// ✗ sans le mot : un callback dans un callback dans un callback
get(a, r1 => get(r1, r2 => get(r2, r3 => afficher(r3))))
La promesse, elle, nomme « une valeur qui arrivera plus tard ». Le mot-clé await veut dire « attends ici que la valeur arrive, puis donne-la-moi comme une variable normale ». Le même enchaînement se lit alors de haut en bas, comme du code ordinaire :
const r1 = await get(a); // attends le 1er résultatconst r2 = await get(r1); // puis le 2e, qui dépend du 1er
afficher(await get(r2)); // puis le 3e
Même travail pour la machine, même attente réseau : seul le nom a changé ce qu'on peut écrire et suivre. Interface ou promesse, c'est la même leçon : un nom bien choisi vous fait oublier la plomberie et penser plus haut. C'est tout le chapitre en une idée.
On sait maintenant tout exprimer. Mais tout exprimer et l'exprimer clairement sont deux choses différentes : un programme juste peut rester un casse-tête à relire. Le prochain enjeu n'est plus le pouvoir du langage, c'est la clarté.
3
Écrire pour les humains
la propreté
Le code est lu bien plus souvent qu'il n'est écrit. La machine, elle, se moque de l'élégance : un nom à une lettre s'exécute aussi vite qu'un nom parlant. La propreté n'est donc pas pour la machine : c'est de la communication avec le prochain humain qui ouvrira le fichier. Et ce prochain humain, c'est souvent vous. Cette propreté se construit en six gestes : le plus petit, bien nommer une variable ; le plus grand, faire évoluer le système entier sans le casser.
3.1 Le nom révèle l'intention
Un bon nom rend le commentaire inutile. Il doit répondre à trois questions d'un coup :
pourquoi ça existe ;
ce que ça fait ;
comment on s'en sert.
Comparez la condition brute et la même intention nommée :
// ✗ on doit décoder la règle métier à chaque lectureif (age >= 18 && solde > 0 && !suspendu) { ... }
// ✓ le nom EST l'explication ; la règle vit à un seul endroitif (peutPasserCommande(client)) { ... }
C'est le pouvoir de nommer du chapitre 2, redescendu à l'échelle d'une seule variable.
Ici deux maîtres s'affrontent, et la dispute est instructive :
Clean Code : les fonctions doivent être petites, puis plus petites encore ;
A Philosophy of Software Design : passé un point, découper encore n'aide plus personne.
La vraie question n'est pas « combien de lignes ? », mais « est-ce plus simple pour qui l'appelle ? ». Prenons un prix à calculer : HT, TVA, remise, port.
// ✗ sur-découpé : 4 micro-fonctions que l'appelant doit enchaîner à la mainconst ht = lirePrix(c);
const tva = appliquerTVA(ht);
const remise = retirerRemise(tva, c);
const total = ajouterPort(remise, c);
// ✓ une fonction "profonde" : un seul appel, les 4 étapes cachées dedansconst total = prixFinal(c);
Le découpage en lui-même n'est pas le problème : prixFinal peut très bien appeler ces quatre étapes en interne. Ce qui change, c'est qu'elles deviennent privées ; l'appelant ne voit qu'un seul nom au lieu d'orchestrer la chaîne lui-même.
La fonction profonde offre ainsi une toute petite surface (un nom, un argument) pour beaucoup de travail caché. Découper, oui ; l'exposer, non. On ne fusionne que dans un cas : deux étapes si soudées qu'on ne comprend pas l'une sans l'autre. Les séparer créerait des méthodes siamoises, le sur-découpage que dénonce A Philosophy of Software Design.
À taille égale, la fonction profonde cache le plus : une surface minuscule (prixFinal(c)) pour tout le calcul derrière. La peu profonde oblige à connaître ses quatre rouages.
Sur les commentaires, Clean Code est cinglant : Robert Martin va jusqu'à écrire que les commentaires sont toujours des échecs, la preuve qu'on n'a pas su s'exprimer dans le code. Un commentaire qui ne fait que paraphraser le code lui donne raison : il vieillit et finit par mentir, car le code change et lui non.
i++; // ✗ incrémente i (le code le dit déjà : inutile)
i++; // ✓ on saute l'octet d'en-tête, les paquets mal formés l'omettent
Mais tout bannir serait l'excès inverse. A Philosophy of Software Design, de John Ousterhout, rétablit l'équilibre : le bon commentaire dit ce que le code ne peut pas dire. Trois genres méritent qu'on les écrive :
le pourquoi : la décision non évidente, le compromis derrière le code. // en centimes : zéro arrondi flottant
l'avertissement : le piège, l'ordre qu'on ne doit pas casser. // NE PAS réordonner : valider avant d'enregistrer
le contrat : ce qu'une fonction promet à qui l'appelle (ce qu'elle attend, ce qu'elle renvoie, ses effets), pour ne pas avoir à lire son corps.
C'est même un détecteur : quand un commentaire devient long et pénible à écrire, c'est « le canari dans la mine », le signe que votre abstraction (la façon dont vous avez découpé le problème) est mauvaise. Ousterhout va jusqu'à les écrire avant le code, comme outil de conception.
L'idée la plus sous-estimée : la meilleure gestion d'erreur, c'est l'erreur qui n'existe pas. Plutôt que d'attraper une exception partout, on redéfinit la sémantique (le sens même de l'opération) pour que le cas d'erreur devienne un cas normal.
// ✗ Java : lève une exception, à blinder de vérifications partout"hi".substring(0, 10); // 💥 IndexOutOfBounds# ✓ Python : une tranche hors limites se borne à ce qui existe, sans erreur"hi"[0:10] # → "hi"
Et ça vaut dans votre propre code, pas seulement dans la bibliothèque standard d'un langage (les fonctions livrées avec lui). Plutôt que d'imposer à tous les appelants un if (user == null) throw répété partout, renvoyez un objet « invité » qui répond comme un vrai utilisateur :
// ✗ chaque appelant doit se rappeler de tester le nullif (user == null) throw ...; user.nom();
// ✓ "Null Object" : pas d'utilisateur = un Invité, qui sait répondre
user.nom(); // → "Invité", droits vides : le cas d'erreur a disparu
C'est la même intuition que « tirer la complexité vers le bas » (l'absorber dans le module plutôt que la repousser vers ceux qui l'appellent) : qu'une équipe souffre une fois dedans, plutôt que mille appelants dehors.
DRY (« Don't Repeat Yourself », ne te répète pas) est l'un des principes les plus mal compris. Ce n'est pas « ne copie-colle pas du code », c'est « chaque savoir a une seule source d'autorité dans le système ». Le mot qui compte est savoir, pas code, et deux conséquences contre-intuitives en découlent.
Du code identique n'est pas toujours une duplication. Si deux fragments se ressemblent par hasard et évolueront pour des raisons sans rapport, les fusionner les couple à tort. Le prix d'un article se valide avec prix > 0, sa quantité en stock avec quantite > 0 : tentant de tout réunir dans un seul estPositif(). Mais demain le stock devra accepter 0 (rupture de stock), pas le prix : c'étaient deux règles, identiques par coïncidence.
Et on se répète sans copier une seule ligne. Le même savoir fuite ailleurs : une règle de validation refaite côté client et côté serveur, la structure d'une table que le code re-décrit à la main, ou un commentaire qui redit ce que le code fait déjà, exactement le piège du commentaire-paraphrase vu plus haut. Aucun copier-coller, et pourtant deux vérités à garder synchronisées.
La parade tient en un mot : chaque savoir a un seul foyer ; tout le reste du code s'y réfère, au lieu de re-savoir la même chose dans son coin.
Et derrière DRY se tient une valeur plus large, ETC (Easier To Change, plus facile à changer). Ce n'est pas une règle de plus, c'est une boussole : à chaque choix, une seule question, « est-ce que ça rendra le système plus facile, ou plus dur, à modifier ? ». Le découplage, les bons noms, DRY lui-même n'en sont que des cas particuliers, et c'est elle qui portera tout le chapitre suivant : l'architecture, c'est ETC à l'échelle du système.
// ✗ chaque nouveau canal rouvre la fonctionif (canal == "email") ... else if (canal == "sms") ...
// ✓ une table {canal → notifieur} : un nouveau canal = une ligne, la logique ne bouge pas
notifieurs[canal].envoyer(message)
3.6 Refactorer, c'est changer la forme sans changer le comportement
Refactorer n'est pas « réécrire » : c'est transformer la structure interne sans toucher au comportement observable. Un test qui passait avant passe encore après ; sinon ce n'est plus un refactoring, c'est une modification.
On avance par petits pas sûrs, guidés par les « odeurs » (ces signes qui trahissent un code mal structuré). L'une des plus courantes, le Data Clump : un groupe de paramètres qui voyagent toujours ensemble réclame une classe.
livrer(nom, rue, ville, cp) // ✗ 4 paramètres collés partout
livrer(adresse) // ✓ ils formaient un concept : une classe Adresse
Et le geste clé, contre-intuitif, tient dans une phrase de Kent Beck : rendre le changement facile, puis faire ce changement devenu facile (réorganiser d'abord, c'est parfois ça le plus dur). Vous devez brancher le paiement PayPal ? D'abord vous refactorez pour qu'un moyen de paiement devienne interchangeable, sans toucher au comportement ; ensuite vous ajoutez PayPal presque gratuitement.
Mais ces petits pas ne sont sûrs qu'avec un filet. Fowler le martèle : sans une batterie de tests qui confirme à chaque étape que le comportement observable n'a pas bougé, le refactoring devient un pari à l'aveugle (on y revient au chapitre 5).
Et le réflexe quotidien tient en une image, la règle du boy-scout de Clean Code : laissez toujours le fichier un peu plus propre que vous ne l'avez trouvé. Pas tout nettoyer, juste vos déchets : la propreté devient une marée, pas un grand ménage qu'on repousse sans fin.
On sait écrire une fonction, un fichier propre. Mais mille fonctions propres ne font pas un système clair : il leur manque la forme d'ensemble, celle qui décide quoi dépend de quoi et où passent les frontières. Cette forme, c'est l'architecture. Sans elle, les raccourcis locaux s'accumulent en dette technique, jusqu'à figer le tout.
4
Donner une forme au système
architecturer
À l'échelle du système, une question domine : qui dépend de qui, et qu'est-ce qui peut changer sans tout casser ? L'architecture, c'est décider de la forme des dépendances avant qu'elles ne se décident toutes seules, en désordre. Quatre niveaux de réponse : réutiliser sans hériter, reconnaître les patterns, inverser les dépendances, peser les compromis ; et au sommet, un seul esprit qui garde la forme.
Composer des pièces aux jointures nettes tient ; un bloc monolithique rigide se fend au premier changement.
4.1 Composer plutôt qu'hériter
Le premier réflexe pour réutiliser, c'est l'héritage : une classe enfant hérite d'un parent. Le piège : elle hérite de tout, même de ce qu'elle ne veut pas, et un changement du parent casse l'enfant à distance. L'exemple culte de Head First : tous les canards héritent de voler()… puis débarque le canard de bain, qui en hérite aussi alors qu'il ne vole pas. La composition règle ça : voler() devient un objet à part que le canard possède au lieu d'en hériter, qu'on lui branche et qu'on remplace (vrai vol, ou aucun) sans toucher au canard, même pendant que le programme tourne.
// ✗ héritage : voler est figé dans la hiérarchieclass CanardDeBain extends Canard { } // hérite voler()… mais ne vole pas !// ✓ composition : le comportement est un objet injecté, changeable
canard.comportementVol = new NePeutPasVoler()
« Préférez la composition à l'héritage » est le tout premier principe des patterns.
Un design pattern n'est pas une décoration qu'on plaque pour faire sérieux : c'est une réponse éprouvée à un problème récurrent.
Stratégie : changer d'algorithme à la volée ;
Observateur : prévenir une liste d'abonnés quand un état change ;
Décorateur : empiler des responsabilités sans sous-classer.
Aucun n'est sorcier ; l'Observateur, par exemple, n'est qu'une liste de fonctions qu'on rappelle à chaque changement :
sujet.abonnés = [majAffichage, envoiMail]
sujet.changer(état) { abonnés.forEach(fn => fn(état)) } // tout le monde est prévenu
Le GoF (le « Gang of Four », les quatre auteurs du livre fondateur Design Patterns) range ses 23 patterns en trois familles, et ce cadre vaut mieux que la liste :
Création (comment instancier) : Fabrique, Singleton, Monteur ;
Vous ne mémorisez pas 23 recettes : vous posez une question, « mon problème est de créer, d'assembler ou de communiquer ? », et le couloir se réduit à quelques candidats.
Au-dessus de tous, le principe fondateur du livre : programmer vers une interface, pas vers une implémentation. Votre code travaille avec ce qu'un objet sait faire, jamais avec ce qu'il est.
// ✗ vers l'implémentation : le code est soudé à une classe précise
exporter(doc) { new PDF().écrire(doc) } // ne sait faire que du PDF// ✓ vers l'interface : « quelque chose qui sait écrire() »
exporter(doc, sortie) { sortie.écrire(doc) } // PDF, CSV, HTML… : ce qu'elle FAIT, pas ce qu'elle EST
Et la règle finale : on n'applique pas un pattern, on le reconnaît quand le besoin l'appelle. Le danger n°1 est la sur-application : forcer un problème dans un pattern là où une solution simple suffirait, comme une Fabrique qui n'emballe qu'un seul new.
4.3 Dépendre d'abstractions, inverser les dépendances
Ici, tout se joue sur le sens des dépendances. La règle de dépendance de Clean Architecture : le code stable et important (les règles métier) ne doit jamais dépendre du code volatil (la base, le framework, l'UI). On inverse le sens habituel : le métier déclare une interface, l'infrastructure s'y plie.
// le métier déclare CE DONT il a besoin (une interface)interface DépôtClient { trouver(id): Client }
// l'infra (SQL, API…) l'implémente ; le métier ignore tout d'elle// → on change de base de données sans toucher une ligne de métier
Le métier déclare l'interface, la base s'y plie : la flèche de dépendance se retourne. Le code métier ne dépend plus de rien de volatil.
C'est le « il me faut quelque chose qui sait faire X » du chapitre 2, monté à l'échelle d'un système : les frontières protègent ce qui compte du reste. Martin va plus loin avec une formule-choc : la base de données et le framework sont des détails, au même titre que la marque des câbles électriques d'une maison. On ne conçoit pas la maison autour des fils ; votre code métier ne devrait même pas savoir que MySQL existe.
Le SOLID formalise ça, cinq règles, une par lettre :
S (Single Responsibility) : une classe, une seule raison de changer ;
O (Open/Closed) : ouverte à l'extension, fermée à la modification ;
L (Liskov Substitution) : un sous-type doit pouvoir remplacer son parent sans surprise ;
I (Interface Segregation) : plein de petites interfaces valent mieux qu'une énorme ;
D (Dependency Inversion) : dépendre de l'abstraction, jamais du concret (ce qu'on vient de voir).
4.4 Pas de « best practice », seulement le moins pire des compromis
Plus on monte, moins il y a de réponses universelles. Hard Parts le pose net : « pour les architectes, chaque problème est un flocon de neige ». La compétence n'est pas de choisir le bon pattern, c'est de peser les compromis. Deux outils pour ça.
Le premier, le quantum d'architecture : le plus petit morceau qu'on peut déployer, tester et faire tomber seul. Deux services qui partagent la même table SQL, c'est comme deux appartements derrière un seul disjoncteur : l'un coupe le courant, l'autre est dans le noir. Ils ne forment qu'un seul quantum.
Le test, devant n'importe quel schéma : « si ce composant change ou crashe, combien d'autres tombent avec lui ? » Ce nombre, c'est la taille de votre quantum. Vingt « microservices » sur une base commune ? Un seul quantum, là encore.
Le quantum, c'est le rayon de la panne : ce qui partage une base tombe d'un bloc. Vingt « microservices » sur une base commune ne font qu'un seul quantum.
Le second, « la réutilisation, c'est du couplage » : partager une classe métier entre services propage chaque changement partout à la fois. D'où un réflexe contre-intuitif : on duplique parfois exprès. On ne met en commun que ce qui est vraiment un seul savoir, tenu de rester cohérent (le DRY du chapitre 3) ; pour deux bouts qui se ressemblent mais évolueront à part, un peu de copie vaut mieux qu'un mauvais couplage.
Aucun des deux ne se tranche par principe. Le petit quantum achète de l'indépendance, la mise en commun achète de la cohérence, et chacune se paie : l'une en duplication et en réseau, l'autre en couplage. On choisit au cas par cas, selon ce qui compte le plus ici : pas de règle, seulement le moins pire des compromis.
La plus belle architecture meurt si quarante équipes empilent leurs idées sans coordination : l'API où chacune nomme « identifiant » autrement et formate les dates à sa façon. Chaque morceau marche seul ; l'ensemble est illisible.
// ✗ trois équipes, trois noms pour le même identifiant, trois dates
GET /users → { id, "2026-06-09" }
GET /orders → { userId, "09/06/2026" }
GET /cart → { uid, 1749427200 }
// ✓ un seul vocabulaire, un seul format : l'appelant devine tout
GET /users · /orders · /cart → { id, "2026-06-09" }
Le Mythe du mois-homme, de Fred Brooks, tranche : « l'intégrité conceptuelle est la considération la plus importante dans la conception d'un système ». Elle doit sortir d'un seul esprit, ou d'un très petit groupe, sinon c'est la tour de Babel.
Un seul esprit ne veut pas dire une seule personne qui code tout : cette poignée décide de la forme, les autres la remplissent. Et Brooks insiste : « la forme libère ». Une fois la structure fixée, chacun sait où sa pièce s'emboîte, et code plus vite, pas plus lentement.
Une architecture, même élégante, n'est qu'une hypothèse tant qu'elle n'est pas prouvée. Et la règle de dépendance qu'on vient de poser ne se vérifie pas à l'œil : il faut un test qui échoue si une frontière est franchie. Prouver, livrer, tenir sous la charge réelle : la forme doit affronter le monde.
5
Prouver, livrer, tenir la charge
le flux
Du code qui « marche sur ma machine » à un service qui tient en production devant des milliers d'utilisateurs, il y a un gouffre. Le franchir, ce n'est pas coder plus : c'est installer un flux qui prouve, livre et tient, sans héroïsme et sans nuits blanches.
Le flux régulier livre sans effort ; la file saturée fige tout, même quand chacun est à fond.
5.1 Le test d'abord
Écrire le test avant le code inverse l'ordre habituel, et ça change tout : on définit le résultat attendu avant de savoir comment l'obtenir. Le cycle se répète à l'infini : Rouge (un test qui échoue), Vert (le code le plus bête qui le fait passer, même en dur), Refactor (nettoyer sans casser).
// 1. ROUGE : le test AVANT le code
test('5 + 3 = 8', () => expect(somme(5, 3)).toBe(8)) // ✗ échoue// 2. VERT : le code le plus bête qui passe (oui, "8" en dur)function somme(a, b) { return 8 } // ✓ vert, sans honte// 3. REFACTOR : un 2e test casse le "8", on généralisefunction somme(a, b) { return a + b } // ✓ propre, toujours vert
On boucle sans fin : un test qui échoue, le code minimal qui le fait passer, puis on nettoie, et on enchaîne le suivant.
On ne refactorise jamais sur du rouge. Le but tient en une formule : du code propre qui marche.
Mais l'effet le plus profond du TDD (Test-Driven Development, le développement piloté par les tests) n'est pas d'attraper des bugs, c'est le design émergent : en écrivant le test d'abord, on conçoit l'API du point de vue de celui qui l'appelle, pas de celui qui l'implémente. Le code devient modulaire et découplé parce qu'il faut qu'il soit testable.
On ne dessine pas l'architecture en avance, on la laisse apparaître, test après test. C'est ce qui rend les chapitres 3 et 4 plus faciles à tenir.
5.2 Versionner, c'est un système adressable par contenu
Le versioning est le filet du flux : on change tout, on tente, on travaille à plusieurs sans se marcher dessus, et on peut toujours revenir en arrière. La preuve par la peur : vous venez d'écraser trois jours de travail d'un git reset --hard mal visé. Panique. Sauf que dans Git, presque rien ne disparaît, et comprendre pourquoi change tout.
Git n'est pas un dossier d'historique : c'est une base d'objets, chacun adressé par son empreinte : un condensé court calculé à partir du contenu (le « hash », produit par la fonction SHA-1). Le même contenu donne toujours le même hash, donc rien ne se perd ni ne se falsifie en silence. Tout s'enchaîne par pointeurs :
Tout est un objet immuable, nommé par son empreinte. Une branche n'est qu'un pointeur qu'on déplace ; rien ne s'efface vraiment.
git cat-file -p HEAD # affiche ce commit : son tree, son parent, l'auteur
Et avant d'entrer dans ce graphe, un fichier traverse trois zones, ce qui explique enfin pourquoi git add existe :
git add photographie une version pour le prochain commit ; git commit la scelle dans le graphe. D'où la possibilité de ne commiter qu'une partie.
git add ne sauvegarde rien : il photographie la version exacte d'un fichier pour le prochain commit. Le commit scelle cette photo dans le graphe. C'est pour ça qu'on peut commiter une partie de ses changements seulement.
Et vos trois jours effacés ? Le commit d'avant le reset est toujours là, objet immuable dans la base ; git reflog liste les hash récents, vous repointez une branche dessus, et tout revient. Comprendre le graphe, c'est livrer sans paniquer.
The Phoenix Project enseigne par le roman une loi d'usine implacable : le temps d'attente d'une tâche explose à mesure qu'une ressource (un serveur, une équipe, une personne dont tout dépend) s'approche de 100 % d'occupation.
temps d'attente ≈ % occupé ÷ % libre // la loi d'attente
Presque plat, puis vertical : 50/50 = 1×, 90/10 = 9×, 99/1 = 99×. La marge n'est pas du gâchis, c'est elle qui fait circuler le travail.
Pourquoi ce mur ? À 99 % d'occupation, plus aucune marge n'absorbe l'imprévu : une tâche qui traîne, un pic d'arrivées, et la file enfle sans jamais se résorber. À 50 %, le temps libre lisse ces à-coups au fil de l'eau.
« Tout le monde est à fond » et « rien n'avance » sont donc la même phrase. Le remède est contre-intuitif : limiter le travail en cours, arrêter d'en commencer pour enfin en finir. La marge n'est pas du gâchis : c'est elle qui fait circuler le travail.
5.4 Déployer souvent fait moins peur que déployer rarement
Le grand déploiement préparé pendant des mois est un tir de canon : une fois parti, plus rien ne se corrige. Une rafale de petites livraisons, elle, s'ajuste en continu.
Accelerate le mesure : les équipes qui déploient souvent et avec un couplage faible (livrer sans demander la permission à une autre équipe, le quantum du chapitre 4) sont à la fois plus rapides et plus stables, ce n'est pas un compromis. On y arrive avec des outils comme les feature flags (livrer du code éteint, qu'on allume ensuite d'un interrupteur) ou le déploiement blue-green (basculer entre deux versions en une bascule réversible).
Et surtout, Accelerate donne quatre chiffres pour se mesurer objectivement, les métriques DORA (DevOps Research and Assessment, l'équipe de recherche derrière le livre) :
lead time : temps entre un commit et sa mise en prod ;
fréquence de déploiement : à quelle cadence on livre ;
MTTR (Mean Time To Recovery) : temps pour rétablir le service après une panne ;
taux d'échec des changements : quel % de déploiements casse quelque chose.
Les équipes d'élite livrent plusieurs fois par jour et se rétablissent en moins d'une heure ; les plus faibles livrent tous les six mois (enquêtes State of DevOps, des milliers d'équipes sondées entre 2014 et 2017). Et les mêmes pratiques réduisent le burnout : la « douleur de déploiement » prédit l'épuisement des équipes.
5.5 La lecture se scale en copiant, l'écriture en découpant
Le service tient le déploiement ; reste la charge. Un million de lecteurs sur une seule base, et chaque requête fait la queue dans la même file. La parade côté lecture : copier. La réplication duplique la base sur plusieurs machines, qui servent toutes des lectures ; le cache garde les réponses déjà calculées tout près (Redis, un CDN : la pyramide du chapitre 1, à l'échelle du datacenter) ; l'index évite de parcourir la table entière, comme celui d'un livre évite de feuilleter 500 pages.
Copier n'aide pas l'écriture : chaque copie devrait encaisser chaque écriture. Elle, on la découpe. Le sharding répartit les données par tranches (clients A-M ici, N-Z là), et chaque machine n'encaisse que sa part. Et c'est ici que les ennuis commencent : une donnée éclatée sur plusieurs machines, c'est exactement la situation où les garanties meurent.
Sur une seule base de données, vous vivez protégé sans le savoir. Une transaction y est tout-ou-rien : le virement débite ET crédite, ou ne fait rien. C'est le contrat ACID (une transaction atomique, cohérente, isolée, durable), et le moteur vous l'offre gratuitement : si l'étape 2 échoue, il annule l'étape 1 tout seul (le rollback).
Puis le service grossit, la donnée se répartit sur plusieurs machines, et ce contrat meurt en silence : plus personne ne peut annuler « tout », chaque machine ne voit que son morceau.
Des anomalies neuves apparaissent, invisibles sur une seule base. La plus vicieuse, le write skew : deux transactions, chacune parfaitement valide, qui cassent une règle ensemble. L'exemple du livre : un hôpital exige au moins un médecin de garde. Alice et Bob, les deux derniers de garde, se désinscrivent au même moment. La transaction d'Alice vérifie « Bob est encore là ? oui » et valide ; celle de Bob vérifie « Alice est encore là ? oui » et valide. Chacune a vu un monde où la règle tenait ; les deux ensemble laissent zéro médecin. Aucune erreur n'a été levée nulle part.
Vert + vert = rouge : chaque transaction a validé un monde où la règle tenait encore ; ensemble, elles la cassent sans déclencher la moindre erreur.
Faute de rollback global, on écrit la marche arrière à la main : c'est la saga. Commander = réserver le stock ①, débiter la carte ②, créer la livraison ③ ; si ③ échoue, votre code déclenche lui-même le remboursement de ② puis la libération de ①. Ce que le moteur faisait gratuitement devient votre travail, étape par étape.
La morale du distribué tient en une ligne : le réseau ment, l'horloge ment ; « la suspicion et la paranoïa paient ».
Un système prouvé, livré et tenu sous la charge est enfin prêt à rencontrer ses vrais usagers. Et c'est précisément là que les certitudes techniques se heurtent au réel : un humain pressé qui ne lit pas, une équipe qui grandit, un attaquant qui sonde. La technique n'était que le moyen ; le produit pour des humains, voilà la fin.
6
Le logiciel est pour des humains
produit, équipe, attaquant
Tout ce qui précède sert un but unique : un humain, devant l'écran, qui veut faire quelque chose. Et autour de lui, d'autres humains : l'équipe qui construit, l'autre développeur qui appelle votre API, et l'attaquant qui cherche la faille. Ce chapitre regarde le code par les yeux de ces gens-là.
Le bon logiciel se prend en main sans notice, par tout le monde, y compris ceux qu'on oublie.
6.1 Ne me fais pas réfléchir
Un utilisateur ne lit pas une page, il la scanne, et chaque demi-seconde d'hésitation est une friction qui le fait partir. D'où la loi que l'ergonome Steve Krug a posée en 2000, et qui donne son titre au livre : ne me fais pas réfléchir. Les conventions battent la créativité (la loupe en haut à droite, le panier juste à côté) parce que l'utilisateur les trouve sans y penser, sur tous les sites qu'il connaît déjà.
Et chaque visiteur arrive avec un réservoir de bonne volonté que chaque mauvaise décision vide. Vous avez mis vingt minutes à remplir votre panier ; total 45 € ; vous cliquez « commander » ; frais de livraison : 12 €. Vous fermez l'onglet. Voilà le réservoir à sec, en un seul chiffre caché trop tard. Le remplir, c'est l'inverse : être transparent, pardonner une faute de format, ne jamais lui barrer la route avec une animation.
Une API (interface de programmation) est le contrat par lequel un programme en appelle un autre, le plus souvent par-dessus le web. Son vocabulaire universel, ce sont les verbes HTTP : GET pour lire, POST pour créer, PUT/PATCH pour modifier, DELETE pour supprimer. Ils disent l'intention sans qu'on lise la doc : GET /users/42 se devine seul. Et la règle d'or d'Arnaud Lauret, l'auteur du livre, est le consumer-first : on ne part pas de sa base de données, on part de ce que l'appelant veut faire, on dessine la réponse idéale pour lui, puis on construit à rebours, jusqu'à la base de données.
Au-delà : des noms prévisibles donnent des superpouvoirs (on devine le reste) ; une erreur doit être généreuse (dire ce qui ne va pas, où, et tout d'un coup) ; et la donnée la plus sûre est celle qui n'existe pas, n'exposez que le strict nécessaire.
// ✗ deviné par personne // ✓ deviné par tous, et standard
{ "ACTBLNDFPRTF": true } { "overdraftFacility": { "active": true } }
Une personne aveugle ne voit pas votre bouton ; son lecteur d'écran le lui annonce, en trois informations : un nom (le texte lu), un rôle (quel genre de chose c'est), un état (coché, ouvert, désactivé).
Un <div> cliquable n'a ni rôle ni état : pour le lecteur d'écran, il n'existe pas. D'où la première règle d'ARIA (Accessible Rich Internet Applications, les attributs aria-* du code ci-dessus) : utiliser le bon élément HTML, qui fournit les trois gratuitement. Tout ça est cadré par un référentiel mondial, les WCAG (Web Content Accessibility Guidelines), quatre principes (Perceptible, Utilisable, Compréhensible, Robuste) et trois niveaux d'exigence (A, AA, AAA). Deux gestes concrets qui couvrent l'essentiel : un contraste suffisant (ratio 4,5:1 sur le texte), et la navigation au clavier.
/* ✗ l'erreur la plus répandue : on efface le contour de focus */
button:focus { outline: none; }
/* ✓ un focus visible : l'utilisateur au clavier voit où il est */
button:focus { outline: 2px solid #005fcc; }
Le test ultime, gratuit : posez la souris, parcourez votre page à la touche Tab. Si vous perdez le fil, un utilisateur au clavier aussi.
Jusqu'ici, on a regardé le logiciel par le dehors : l'utilisateur, puis le développeur qui appelle l'API. Changeons de figure : qui construit ce logiciel ? L'organisation elle-même est une interface. Loi de Conway : un système copie la structure de communication de l'organisation qui le construit. Quatre équipes qui se parlent mal produiront quatre modules qui s'emboîtent mal, qu'on le veuille ou non.
Le même dessin que le quantum (ch. 4), côté humains : une table partagée couple les trois équipes de force ; trois bases, et chacune livre seule.
Team Topologies retourne la loi en levier : si vous voulez une certaine architecture, organisez d'abord les équipes pour l'obtenir. La contrainte cachée est la charge cognitive : une équipe ne peut tenir qu'une quantité limitée de domaine. On découpe alors le système le long de ses plans de fracture, ses coutures naturelles, le plus souvent le domaine métier. Et le livre donne le mode d'emploi, quatre types d'équipes :
alignée sur un flux : une équipe = un produit, livré seule de bout en bout (le cas par défaut) ;
plateforme : fournit des outils internes pour que les autres livrent sans attendre ;
habilitante : aide une équipe à monter en compétence, puis se retire ;
sous-système compliqué : maintient un morceau trop pointu pour être partagé (un moteur de calcul, par exemple).
6.5 Ajouter des gens à un projet en retard le retarde
L'intuition dit : projet en retard, on ajoute des développeurs. C'est faux, et ça porte un nom, la loi de Brooks. Le travail se divise mal, chaque nouvelle personne doit être formée (par les anciens, qu'on ralentit donc), et surtout elle multiplie les canaux de communication.
canaux de communication = n × (n − 1) ÷ 2
5 personnes → 10 canaux // gérable
15 personnes → 105 canaux // la moitié du temps part en coordination
De 3 à 6 personnes, les canaux passent de 3 à 15 : chaque arrivée multiplie les traits, pas seulement les bras. À 15 personnes : 105 canaux.
« Le mois-homme est une unité mythique » : homme et mois ne sont pas interchangeables. Neuf femmes ne font pas un bébé en un mois.
Dernière figure du chapitre, la moins aimable : l'attaquant. On ne défend bien que ce qu'on sait attaquer. Les failles classiques sont toutes une variante du même péché : une donnée venue de l'utilisateur traitée comme du code, ou crue sur parole. XSS (du HTML injecté qui s'exécute), injection SQL (une entrée qui referme la requête et enchaîne la sienne : '; DROP TABLE users --), ou mass assignment, un champ glissé dans la requête qui promeut son envoyeur :
POST /api/profil { "nom": "Alice", "isMember": true }
user.update(req.body) // ✗ isMember passe → Alice s'auto-promeut
La parade : ne jamais faire confiance à l'entrée, et la défense en profondeur, où chaque couche se protège elle-même (validation en entrée, et requêtes préparées en base, et en-têtes de sécurité côté serveur : si l'une cède, les autres tiennent). Et le bon vocabulaire : des « mitigations », pas des « fixes », car aucune défense n'est jamais définitive.
Tout ce métier, des bits jusqu'à l'équipe, vient d'être bousculé par un acteur entièrement neuf, capable d'écrire du code à la demande : l'IA. Elle ne remplace pas le savoir d'avant, elle le rend plus nécessaire que jamais : quelqu'un doit juger ce qu'elle produit, et bien juger exige précisément tout ce qu'on vient de gravir.
7
Coder à l'ère de l'IA
le dernier maillon
Un nouvel étage s'est posé sur les six précédents : une machine qui écrit du code à la demande. La question n'est plus « est-ce que l'IA sait coder ? » (oui, souvent), mais « que reste-t-il à l'humain ? ». La réponse est troublante, parce qu'on attendait du technique et qu'elle est humaine : tout ce que vous venez de lire.
Mais d'abord, dissipons LA confusion de débutant. Faire de l'IA aujourd'hui, ce n'est pas entraîner un modèle, c'est en appeler un déjà entraîné et bâtir par-dessus. Vous ne fabriquez pas votre moteur de recherche, vous appelez une API ; l'ingénieur IA n'entraîne pas le réseau de neurones, il orchestre des appels à un modèle existant et conçoit tout autour. C'est le sens du mot engineering, et tout ce chapitre vit à ce niveau-là.
L'IA tend les instruments à toute vitesse ; l'humain tient le scalpel et décide du geste.
7.1 L'IA génère du probable, pas du vrai
Un modèle de langage ne sait pas ce qui est vrai : il produit, mot après mot, le plus probable. L'hallucination n'est donc pas un bug à corriger un jour, c'est le mécanisme même qui le fait fonctionner : « tout ce qui a une probabilité non nulle, aussi faux soit-il, peut être généré ». Demandez-lui l'auteur d'un livre obscur : il vous donnera, avec le même aplomb, un nom plausible et faux. La conséquence pratique gouverne tout le reste : on ne fait jamais confiance les yeux fermés, on vérifie, et on conçoit le système autour de cette incertitude plutôt que contre elle.
Un modèle se trompe d'abord quand il lui manque l'information. Lui donner le bon contexte au bon moment est devenu LA compétence. Le pattern RAG (Retrieval-Augmented Generation : générer en s'appuyant sur des documents récupérés) : aller chercher les documents pertinents dans une base externe et les coller dans le prompt avant la réponse, pour que le modèle s'appuie sur des faits fournis, pas sur sa mémoire floue.
docs = base.chercher(embed(question), top_k=3) # embed = transforme la question# en vecteur, pour chercher par sens# top_k=3 : garde les 3 plus proches
prompt = f"Réponds avec CECI : {docs}\n\n{question}"# 2. on les colle
reponse = llm(prompt) # 3. réponse sur des faits
Le modèle ne pioche plus dans sa mémoire floue : il lit les documents qu'on vient de lui tendre, puis répond.
Le contexte se soigne aussi dans le prompt lui-même, et la différence est brutale :
# ✗ vague → réponse fantaisiste, format imprévisible"résume ça"# ✓ rôle + contexte + format imposé → réponse exploitable"Tu es juriste. Résume ce contrat en 3 puces, cite la clause de chaque point."
Et le vrai goulot n'est pas le modèle, c'est l'évaluation. Le pipeline a répondu : la réponse est-elle bonne ? On ne peut pas juste « regarder » ; il faut une grille (les faits sourcés sont-ils là ? rien de faux n'a-t-il été ajouté ? le ton est-il adapté ?). Sans cette grille, on itère à l'aveugle, exactement comme du code sans tests (chapitre 5). C'est aussi là que se joue le choix fine-tuning ou RAG : ré-entraîner un modèle sur ses données, ou simplement lui fournir les bons documents au moment de répondre ; le second suffit dans l'immense majorité des cas.
Brooks rêvait en 1975 d'une équipe chirurgicale : un cerveau qui tient le scalpel (conçoit, décide), entouré de rôles qui amplifient son efficacité, le copilote qui connaît tout le code, le language lawyer qui retient l'API, le toolsmith qui forge les outils. Le rêve butait sur une réalité : il fallait des « bras » à la fois excellents et nombreux, introuvables. L'IA est exactement ces bras. Vous tenez le scalpel ; l'assistant connaît tout le code, génère les scripts, retient l'API par cœur. Le modèle imaginé il y a cinquante ans devient enfin praticable, le jour où les bras sont une machine. L'IA ne supprime pas la loi de Brooks (chapitre 6) : elle en change les paramètres, parce que ces « bras »-là ne coûtent ni formation ni canal de communication.
vérifier qu'il sert l'humain, et résiste à l'attaquant (chapitre 6).
L'IA produit le probable ; vous décidez du juste. Plus elle écrit vite, plus votre jugement devient le maillon rare. Ce livre entier n'apprend pas à coder à la place de l'IA : il apprend à savoir quand elle a tort. Elle ne saura jamais, seule, qu'un montant doit vivre en centimes plutôt qu'en flottant (chapitre 1) : il faut que vous, vous le sachiez.
Et voici le vertige : ce jugement-là, l'IA ne peut pas vous le transmettre, puisque c'est précisément ce qu'elle ne sait pas faire. C'est le seul apprentissage du métier qu'aucun assistant ne raccourcira jamais pour vous : il faut gravir les sept étages soi-même.
Sept niveaux, un seul fil : des nombres dans le silicium jusqu'au jugement qu'aucun modèle ne remplace. À l'arrivée, on ne décide plus à l'aveugle : on sait quoi faire, pourquoi, où, quand et comment. C'est tout le métier, et les fiches ci-dessous tiennent le détail de chaque marche.