Clean Code
Chapitre 5 / 14 · 13 min de lecture

Le formatage

La mise en forme comme acte de communication : ordre vertical, proximité et densité — lire le code comme un journal.

Le formatage du code est trop important pour être ignoré, et trop important pour être traité comme une religion. Sa raison d'être tient en un mot : la communication. Or communiquer est la première mission du développeur professionnel — avant même « faire marcher le code ».

Cette idée surprend. On croit volontiers que la priorité, c'est de livrer une fonctionnalité qui fonctionne. Mais la fonctionnalité d'aujourd'hui a toutes les chances de changer à la prochaine version, tandis que la lisibilité de votre code influencera toutes les modifications à venir. Votre style et votre discipline survivent longtemps après que le code original a été transformé au point d'en devenir méconnaissable. Voyons donc les choix de mise en forme qui aident à communiquer.

Note

Les exemples du livre sont en Java. Nous les transposons ici en TypeScript idiomatique, mais les principes — verticaux et horizontaux — sont indépendants du langage.

La taille des fichiers

Avant de parler de mise en page, parlons de volume. Combien de lignes devrait faire un fichier source ? L'étude de sept projets open-source (JUnit, FitNesse, Tomcat, Ant…) montre une régularité frappante : on peut bâtir des systèmes conséquents — FitNesse approche les 50 000 lignes — avec des fichiers d'environ 200 lignes en moyenne, et 500 au grand maximum.

Ce n'est pas une règle absolue, mais une cible très souhaitable. Les petits fichiers sont presque toujours plus faciles à comprendre que les gros.

Le formatage vertical

La métaphore du journal

Pensez à un bon article de journal. Vous le lisez de haut en bas. Le titre vous dit de quoi il s'agit et vous permet de décider si la suite vous intéresse. Le premier paragraphe résume toute l'histoire à grands traits, en cachant les détails. Plus vous descendez, plus les détails affluent : dates, noms, citations, chiffres.

Un fichier source devrait se lire de la même façon. Son nom doit être simple mais explicatif : il doit, à lui seul, vous dire si vous êtes dans le bon module. Les parties hautes du fichier exposent les concepts de haut niveau et les algorithmes. Le détail augmente au fur et à mesure que l'on descend, jusqu'aux fonctions les plus bas niveau tout en bas.

Si le journal n'était qu'une longue histoire mêlant pêle-mêle faits, dates et noms, personne ne le lirait.

Ouverture verticale entre les concepts

Le code se lit de gauche à droite et de haut en bas. Chaque ligne est une expression ou une clause ; chaque groupe de lignes forme une pensée complète. Ces pensées doivent être séparées par des lignes vides. Chaque ligne vide est un signal visuel qui annonce un concept nouveau et distinct.

Observez l'effet de ces lignes vides entre les déclarations d'import, les propriétés et les méthodes :

// ✅ Avant : l'œil distingue chaque groupe de lignes
export class BoldWidget extends ParentWidget {
	static readonly REGEXP = /'''(.+?)'''/ms;

	constructor(parent: ParentWidget, text: string) {
		super(parent);
		const match = BoldWidget.REGEXP.exec(text);
		this.addChildWidgets(match?.[1]);
	}

	render(): string {
		return `<b>${this.childHtml()}</b>`;
	}
}

Retirez ces lignes vides et le même code devient un magma opaque :

// ❌ Après suppression : tout se confond en un bloc indigeste
export class BoldWidget extends ParentWidget {
	static readonly REGEXP = /'''(.+?)'''/ms;
	constructor(parent: ParentWidget, text: string) {
		super(parent);
		const match = BoldWidget.REGEXP.exec(text);
		this.addChildWidgets(match?.[1]); }
	render(): string {
		return `<b>${this.childHtml()}</b>`; }
}

L'effet est encore plus net quand on défocalise le regard : dans le premier exemple, les groupes de lignes ressortent ; dans le second, tout n'est plus qu'un brouillard.

Densité verticale

Si l'ouverture sépare les concepts, la densité verticale signale au contraire une association étroite. Des lignes fortement liées doivent rester serrées. Voyez comment des commentaires inutiles brisent le lien entre deux propriétés qui vont pourtant de pair :

// ❌ Avant : les commentaires éparpillent ce qui est lié
export class ReporterConfig {
	/** Le nom de classe de l'écouteur de rapport */
	private className: string;

	/** Les propriétés de l'écouteur de rapport */
	private properties: Property[] = [];

	addProperty(property: Property): void {
		this.properties.push(property);
	}
}
// ✅ Après : tout tient dans un seul coup d'œil
export class ReporterConfig {
	private className: string;
	private properties: Property[] = [];

	addProperty(property: Property): void {
		this.properties.push(property);
	}
}

La seconde version « tient dans un regard » : on voit d'un coup une classe avec deux propriétés et une méthode, sans bouger les yeux ni la tête. La première force des allers-retours pour atteindre le même niveau de compréhension.

Distance verticale

Vous est-il déjà arrivé de courir après votre propre queue dans une classe, sautant d'une fonction à l'autre, défilant de haut en bas pour deviner comment les morceaux s'articulent, jusqu'à vous perdre ? C'est frustrant : vous voulez comprendre ce que fait le système, et vous dépensez votre énergie à retrouver où sont les pièces.

Les concepts étroitement liés doivent rester proches verticalement. Leur écart vertical mesure leur importance respective pour la compréhension de l'autre. On veut éviter d'obliger le lecteur à sauter partout dans le fichier.

Déclarer les variables près de leur usage

Les variables se déclarent au plus près de leur utilisation. Comme nos fonctions sont courtes, les variables locales apparaissent naturellement en haut de la fonction :

function countTestCases(tests: Test[]): number {
	let count = 0;
	for (const test of tests) {
		count += test.countTestCases();
	}
	return count;
}

Les variables de contrôle d'une boucle se déclarent dans l'instruction de la boucle elle-même (for (const test of tests)), jamais loin au-dessus.

À l'inverse, les propriétés d'instance se déclarent en haut de la classe. Cela n'augmente pas la distance verticale, car dans une classe bien conçue elles sont utilisées par beaucoup de méthodes, sinon toutes. Le point essentiel : un endroit unique et connu de tous. Le contre-exemple emblématique est cette classe où deux propriétés se cachent à mi-hauteur, perdues entre les méthodes — on ne les découvre que par accident :

// ❌ Avant : propriétés enterrées au milieu des méthodes
export class TestSuite implements Test {
	static createTest(theClass: TestCaseClass, name: string): Test {
		/* ... */
	}

	static warning(message: string): Test {
		/* ... */
	}

	private name: string; // <- introuvable !
	private tests: Test[] = []; // <- introuvable !

	constructor(theClass?: TestCaseClass) {
		/* ... */
	}
}
// ✅ Après : un endroit unique, en haut, connu de tous
export class TestSuite implements Test {
	private name: string;
	private tests: Test[] = [];

	constructor(theClass?: TestCaseClass) {
		/* ... */
	}

	static createTest(theClass: TestCaseClass, name: string): Test {
		/* ... */
	}

	static warning(message: string): Test {
		/* ... */
	}
}

Les fonctions appelantes au-dessus des appelées

Si une fonction en appelle une autre, elles doivent être proches verticalement, et l'appelante au-dessus de l'appelée quand c'est possible. Le programme acquiert ainsi un flux naturel : en lisant une fonction, on sait que les fonctions qu'elle invoque suivront juste en dessous.

export class WikiPageResponder {
	private page: WikiPage | null = null;
	private crawler!: PageCrawler;

	makeResponse(context: FitNesseContext, request: Request): Response {
		const pageName = this.getPageNameOrDefault(request, "FrontPage");
		this.loadPage(pageName, context);
		if (this.page === null) {
			return this.notFoundResponse(context, request);
		}
		return this.makePageResponse(context);
	}

	private getPageNameOrDefault(request: Request, fallback: string): string {
		const pageName = request.getResource();
		return isBlank(pageName) ? fallback : pageName;
	}

	private loadPage(resource: string, context: FitNesseContext): void {
		const path = PathParser.parse(resource);
		this.crawler = context.root.getPageCrawler();
		this.page = this.crawler.getPage(context.root, path);
	}

	private notFoundResponse(context: FitNesseContext, request: Request) {
		return new NotFoundResponder().makeResponse(context, request);
	}

	private makePageResponse(context: FitNesseContext): SimpleResponse {
		const html = this.makeHtml(context);
		const response = new SimpleResponse();
		response.setContent(html);
		return response;
	}
}

On lit de haut en bas, du général au particulier, sans jamais remonter. À noter au passage : la constante "FrontPage" n'est pas enterrée dans getPageNameOrDefault, mais passée depuis l'endroit où il est légitime de la connaître — on garde les constantes au bon niveau.

Affinité conceptuelle

Certains bouts de code veulent être voisins. Plus leur affinité est forte, plus la distance verticale doit être faible. Cette affinité peut venir d'une dépendance directe (un appel, un usage de variable), mais aussi d'un schéma de nommage commun et d'opérations qui sont des variantes d'une même tâche :

export class Assert {
	static assertTrue(condition: boolean, message?: string): void {
		if (!condition) this.fail(message);
	}

	static assertFalse(condition: boolean, message?: string): void {
		this.assertTrue(!condition, message);
	}
}

Ces fonctions ont une forte affinité conceptuelle : elles partagent un nommage et des comportements jumeaux. Le fait qu'elles s'appellent l'une l'autre est secondaire — même sans cela, elles voudraient rester côte à côte.

Ordre vertical : les dépendances vers le bas

De façon générale, on veut que les dépendances d'appel pointent vers le bas : une fonction appelée se trouve sous la fonction qui l'appelle. Comme dans un article de journal, on attend les concepts les plus importants en premier, exprimés avec le moins de détails polluants, et les détails de bas niveau en dernier. On peut ainsi survoler un fichier, en saisir l'essentiel par les premières fonctions, sans plonger dans les détails.

Astuce

Un bon test : ouvrez un fichier que vous ne connaissez pas et lisez-le comme un journal, de haut en bas, sans jamais défiler vers le haut. Si vous comprenez l'essentiel en lisant les premières fonctions, l'ordre vertical est respecté.

Le formatage horizontal

Une largeur de ligne raisonnable

Quelle largeur pour une ligne ? Les programmeurs préfèrent nettement les lignes courtes : dans les projets étudiés, l'immense majorité des lignes font moins de 80 caractères, avec un pic autour de 45. L'ancienne limite Hollerith de 80 est un peu arbitraire ; pousser à 100 voire 120 ne pose pas problème. Au-delà, c'est de la négligence.

À retenir

On ne devrait jamais avoir à défiler horizontalement pour lire une ligne. Que votre écran soit large ne vous autorise pas à étirer le code à 200 caractères. Fixez une limite — 120 est un bon choix — et tenez-vous-y.

Ouverture et densité horizontales

L'espace horizontal sert à associer ce qui est fortement lié et à dissocier ce qui l'est moins. On entoure d'espaces les opérateurs d'affectation pour souligner les deux parties distinctes — gauche et droite :

private measureLine(line: string): void {
	this.lineCount++;
	const lineSize = line.length;
	this.totalChars += lineSize;
	this.histogram.addLine(lineSize, this.lineCount);
	this.recordWidestLine(lineSize);
}

En revanche, pas d'espace entre un nom de fonction et sa parenthèse ouvrante : la fonction et ses arguments sont intimement liés. On sépare en revanche les arguments par une virgule suivie d'un espace, pour montrer qu'ils sont distincts.

L'espacement peut aussi accentuer la précédence des opérateurs. Les facteurs d'une multiplication restent collés (haute précédence), les termes d'une addition sont espacés (basse précédence) :

export class Quadratic {
	static root1(a: number, b: number, c: number): number {
		const det = Quadratic.determinant(a, b, c);
		return (-b + Math.sqrt(det)) / (2*a);
	}

	private static determinant(a: number, b: number, c: number): number {
		return b*b - 4*a*c;
	}
}

Les équations se lisent presque comme des mathématiques. Attention toutefois : la plupart des formateurs automatiques ignorent la précédence et imposent un espacement uniforme, faisant disparaître ces subtilités.

Pas d'alignement horizontal artificiel

La tentation existe d'aligner les noms de variables d'une série de déclarations, ou les valeurs d'une série d'affectations. C'est contre-productif : l'alignement met l'accent sur les mauvaises choses et détourne le regard de l'intention réelle.

// ❌ Avant : l'œil lit la colonne des valeurs sans voir le « = »
class FitNesseExpediter {
	private socket:    Socket;
	private input:     InputStream;
	private output:    OutputStream;
	private request:   Request;

	constructor(s: Socket, context: FitNesseContext) {
		this.context           = context;
		this.socket            = s;
		this.input             = s.getInputStream();
		this.parsingTimeLimit  = 10000;
	}
}
// ✅ Après : déclarations et affectations non alignées
class FitNesseExpediter {
	private socket: Socket;
	private input: InputStream;
	private output: OutputStream;
	private request: Request;

	constructor(s: Socket, context: FitNesseContext) {
		this.context = context;
		this.socket = s;
		this.input = s.getInputStream();
		this.parsingTimeLimit = 10000;
	}
}

Dans la version alignée, on est tenté de parcourir la colonne des noms sans regarder les types, ou la colonne des valeurs sans voir l'opérateur d'affectation. Et si la liste est si longue qu'elle « réclame » un alignement, le vrai problème est sa longueur : la classe devrait sans doute être scindée.

L'indentation : respecter la portée

Un fichier source est une hiérarchie, comme un plan. Pour la rendre visible, on indente chaque ligne en proportion de sa position : les déclarations de classe au niveau du fichier ne sont pas indentées, les méthodes le sont d'un cran, leurs implémentations d'un cran de plus, et ainsi de suite. Les développeurs s'appuient massivement sur ce repère visuel pour sauter d'une portée à l'autre.

Comparez ces deux programmes, syntaxiquement et sémantiquement identiques :

// ❌ Sans indentation : illisible
export class FitNesseServer { private context: Ctx;
constructor(context: Ctx) { this.context = context; }
serve(s: Socket, timeout = 10000) { try { const sender =
new FitNesseExpediter(s, this.context); sender.start(); }
catch (e) { console.error(e); } } }
// ✅ Avec indentation : la structure saute aux yeux
export class FitNesseServer {
	private context: Ctx;

	constructor(context: Ctx) {
		this.context = context;
	}

	serve(s: Socket, timeout = 10000): void {
		try {
			const sender = new FitNesseExpediter(s, this.context);
			sender.setRequestParsingTimeLimit(timeout);
			sender.start();
		} catch (e) {
			console.error(e);
		}
	}
}

Dans la version indentée, on repère instantanément les variables, le constructeur, les méthodes. En quelques secondes, on comprend qu'il s'agit d'un front-end vers une socket avec un délai d'expiration. La version compactée, elle, est impénétrable sans étude intense.

Piège courant

Ne cédez jamais à la tentation de réduire une portée à une seule ligne pour un if court, une boucle brève ou une mini-fonction. Préférez toujours déployer et indenter les corps de bloc, même de deux lignes : la cohérence visuelle prime sur l'économie de hauteur.

Éviter les portées factices

Parfois le corps d'un while ou d'un for est vide. Ces structures sont dangereuses et à éviter. Quand on ne peut pas s'en passer, il faut rendre le corps vide visible par une indentation propre et des accolades, au lieu de le cacher derrière un point-virgule en fin de ligne :

// ❌ Avant : le « rien » se cache, on s'y fait piéger
while (reader.read(buf, 0, bufferSize) !== -1);

// ✅ Après : le corps vide est explicite et indenté
while (reader.read(buf, 0, bufferSize) !== -1) {
	// volontairement vide : on consomme le flux
}

Combien de fois un point-virgule silencieux en fin de while a-t-il trompé son auteur ? Tant qu'on ne le rend pas visible sur sa propre ligne, il reste tout simplement trop difficile à repérer.

Les règles d'équipe

Le titre de cette section est un jeu de mots : chaque programmeur a ses règles de formatage préférées, mais dès qu'il travaille en équipe, c'est l'équipe qui décide (the team rules).

Une équipe doit s'accorder sur un style unique, puis chacun l'applique. On veut un logiciel au style cohérent — pas une œuvre qui semble écrite par une bande d'individus en désaccord. Sur le projet FitNesse, dix minutes ont suffi pour fixer la position des accolades, la taille d'indentation, les conventions de nommage, puis tout encoder dans le formateur de l'IDE.

À faireÀ éviter
Configurer un formateur partagé (Prettier, ESLint)Imposer ses goûts personnels dans un projet collectif
Versionner la config de formatage avec le codeReformater à la main, fichier par fichier
Suivre le style de l'équipe même s'il n'est pas le vôtreMélanger plusieurs styles dans une même base

Le lecteur doit pouvoir faire confiance au fait qu'une convention vue dans un fichier signifie la même chose dans les autres.

Un bon système logiciel est un ensemble de documents qui se lisent agréablement, avec un style cohérent et fluide. La dernière chose à faire est d'ajouter de la complexité en écrivant chaque fichier dans un style différent.

À retenir

  • Le formatage est de la communication, la première mission du professionnel : votre style survit longtemps après le code original.
  • Lisez un fichier comme un journal : nom explicite, concepts de haut niveau en haut, détails en bas, dépendances qui pointent vers le bas.
  • Verticalement : lignes vides entre les concepts, lignes liées serrées, variables près de leur usage, appelante au-dessus de l'appelée.
  • Horizontalement : lignes courtes (≤ 120), espaces autour des opérateurs, jamais d'alignement artificiel, indentation fidèle à la portée.
  • Rendez visibles les portées factices (corps de boucle vide) plutôt que de les cacher derrière un point-virgule.
  • En équipe, un style cohérent prime sur les préférences individuelles : automatisez-le avec un formateur partagé.