Les idées de programmation structurée d'Edsger Dijkstra expliquent pourquoi un code discipliné et simple reste correct et maintenable quand les équipes, les fonctionnalités et les systèmes grandissent.

Les logiciels échouent rarement parce qu’ils ne peuvent pas être écrits. Ils échouent parce qu’un an plus tard, personne ne peut les changer en toute sécurité.
À mesure que les bases de code croissent, chaque ajustement « mineur » commence à se répercuter : une correction de bug casse une fonctionnalité lointaine, une nouvelle exigence force des réécritures, et un simple refactor se transforme en une semaine de coordination prudente. La difficulté n’est pas d’ajouter du code — c’est de maintenir un comportement prévisible pendant que tout autour évolue.
Edsger Dijkstra soutenait que la correction et la simplicité doivent être des objectifs de premier rang, pas de jolis bonus. Le gain n’est pas académique. Quand un système est plus facile à raisonner, les équipes passent moins de temps à circonscrire des incendies et plus de temps à construire.
Quand on dit qu’un logiciel doit « monter en charge », on parle souvent de performance. Le point de Dijkstra est différent : la complexité monte aussi.
La montée en charge se manifeste par :
La programmation structurée n’est pas stricte pour elle-même. Il s’agit de choisir un flux de contrôle et une décomposition qui rendent facile de répondre à deux questions :
Quand le comportement est prévisible, changer devient une routine plutôt qu’un risque. Voilà pourquoi Dijkstra importe encore : sa discipline cible le vrai goulot d’un logiciel qui grandit — le comprendre suffisamment pour l’améliorer.
Edsger W. Dijkstra (1930–2002) était un informaticien néerlandais qui a façonné la façon dont les programmeurs pensent la fiabilité logicielle. Il a travaillé sur des systèmes d’exploitation précoces, contribué à des algorithmes (dont l’algorithme du plus court chemin qui porte son nom) et — surtout pour les développeurs quotidiens — plaidé pour que la programmation soit quelque chose que l’on peut raisonner, et non juste quelque chose qu’on essaie jusqu’à ce que ça marche.
Dijkstra se souciait moins qu’un programme produise la bonne sortie pour quelques exemples, et davantage que l’on puisse expliquer pourquoi il est correct pour les cas importants.
Si vous pouvez énoncer ce qu’un bout de code est censé faire, vous devriez pouvoir argumenter (pas à pas) qu’il le fait réellement. Cet état d’esprit mène naturellement à du code plus facile à suivre, à relire et moins dépendant d’un débogage héroïque.
Certaines écrits de Dijkstra paraissent intransigeants. Il critiquait les astuces « intelligentes », les flux de contrôle négligés et les habitudes de codage qui rendent le raisonnement difficile. Cette sévérité n’est pas du règlement de style ; c’est une réduction de l’ambiguïté. Quand le sens du code est clair, on passe moins de temps à débattre des intentions et plus de temps à valider le comportement.
La programmation structurée consiste à construire des programmes à partir d’un petit ensemble de structures de contrôle claires — séquence, sélection (if/else) et itération (boucles) — au lieu de sauts emmêlés. Le but est simple : rendre le chemin à travers le programme compréhensible pour pouvoir l’expliquer, le maintenir et le modifier en confiance.
La qualité logicielle se décrit souvent par « rapide », « beau » ou « riche en fonctionnalités ». Les utilisateurs vivent la correction différemment : comme l’assurance tranquille que l’application ne va pas les surprendre. Quand la correction est là, personne ne la remarque. Quand elle manque, tout le reste cesse d’avoir de l’importance.
« Ça marche maintenant » signifie souvent que vous avez essayé quelques chemins et obtenu le résultat attendu. « Ça continue de marcher » signifie que le comportement reste conforme aux intentions au fil du temps, des cas limites et des changements — après des refactors, de nouvelles intégrations, une montée en charge et de nouveaux membres d’équipe.
Une fonctionnalité peut « marcher maintenant » tout en étant fragile :
La correction consiste à supprimer ces hypothèses cachées — ou à les rendre explicites.
Un bug mineur reste rarement mineur quand le logiciel grandit. Un état incorrect, un décalage d’un indice ou une règle de gestion d’erreur floue se retrouve copié dans de nouveaux modules, enveloppé par d’autres services, mis en cache, relancé ou « contourné ». Avec le temps, les équipes arrêtent de se demander « qu’est‑ce qui est vrai ? » et se mettent à demander « qu’est‑ce qui arrive généralement ? ». C’est alors que la réponse aux incidents devient de l’archéologie.
Le multiplicateur, c’est la dépendance : un petit mauvais comportement devient de nombreux mauvais comportements en aval, chacun avec son correctif partiel.
Un code clair améliore la correction parce qu’il améliore la communication :
La correction signifie : pour les entrées et situations que nous affirmons supporter, le système produit de manière consistante les résultats promis — et échoue de façon prévisible et explicable quand il ne peut pas.
La simplicité n’est pas rendre le code « mignon », minimal ou astucieux. Il s’agit de rendre le comportement facile à prévoir, expliquer et modifier sans crainte. Dijkstra valorisait la simplicité parce qu’elle améliore notre capacité à raisonner sur les programmes — surtout lorsque la base de code et l’équipe grandissent.
Un code simple maintient un petit nombre d’idées en action à la fois : flux de données clair, flux de contrôle clair et responsabilités nettes. Il n’oblige pas le lecteur à simuler de nombreux chemins alternatifs dans sa tête.
La simplicité n’est pas :
Beaucoup de systèmes deviennent difficiles à changer non pas parce que le domaine est intrinsèquement complexe, mais parce qu’on introduit de la complexité accidentelle : drapeaux qui interagissent de façon inattendue, correctifs de cas particulier qui ne partent jamais, couches existant surtout pour contourner des décisions antérieures.
Chaque exception supplémentaire est une taxe sur la compréhension. Le coût réapparaît plus tard, quand quelqu’un tente de corriger un bug et découvre qu’un changement dans une zone casse subtilement plusieurs autres.
Quand un design est simple, le progrès vient du travail régulier : changements relisables, diffs plus petits et moins de corrections d’urgence. Les équipes n’ont pas besoin de développeurs « héros » qui se souviennent de chaque cas historique ou qui déboguent sous pression à 2h du matin. Au lieu de cela, le système supporte l’attention humaine normale.
Un test pratique : si vous ajoutez sans cesse des exceptions (« sauf si… », « excepté quand… », « seulement pour ce client… »), vous accumulez probablement de la complexité accidentelle. Préférez des solutions qui réduisent le branching comportemental — une règle cohérente vaut mieux que cinq cas particuliers, même si la règle cohérente est un peu plus générale que votre première idée.
La programmation structurée est une idée simple aux grandes conséquences : écrivez du code de sorte que son chemin d’exécution soit facile à suivre. En termes simples, la plupart des programmes peuvent se construire avec trois blocs de base — séquence, sélection, répétition — sans s’appuyer sur des sauts emmêlés.
if/else, switch).for, while).Quand le flux de contrôle est composé de ces structures, on peut généralement expliquer ce que fait le programme en lisant de haut en bas, sans « téléporter » son attention autour du fichier.
Avant que la programmation structurée ne devienne la norme, beaucoup de bases de code s’appuyaient sur des sauts arbitraires (style goto). Le problème n’est pas que les sauts soient toujours mauvais ; c’est que le saut non restreint crée des chemins d’exécution difficiles à prédire. On finit par se demander « Comment en sommes-nous arrivés là ? » et « Quel est l’état de cette variable ? » — et le code n’y répond pas clairement.
Un flux de contrôle clair aide les humains à se construire un modèle mental correct. Ce modèle est ce sur quoi vous vous appuyez quand vous déboguez, relisez une PR ou changez un comportement sous pression.
Quand la structure est cohérente, la modification devient plus sûre : vous pouvez changer une branche sans affecter une autre ou refactorer une boucle sans manquer une sortie cachée. La lisibilité n’est pas que de l’esthétique — c’est la base pour modifier le comportement en confiance sans casser ce qui marche déjà.
Dijkstra défendait une idée simple : si vous pouvez expliquer pourquoi votre code est correct, vous pouvez le changer avec moins de crainte. Trois petits outils rendent cela pratique — sans transformer votre équipe en mathématiciens.
Un invariant est un fait qui reste vrai pendant l’exécution d’un morceau de code, surtout dans une boucle.
Exemple : vous faites la somme des prix d’un panier. Un invariant utile est : « total est la somme de tous les articles traités jusqu’ici. » Si cela reste vrai à chaque étape, alors quand la boucle se termine, le résultat est digne de confiance.
Les invariants sont puissants parce qu’ils focalisent l’attention sur ce qui ne doit jamais se casser, pas seulement sur ce qui devrait arriver ensuite.
Une précondition est ce qui doit être vrai avant l’exécution d’une fonction. Une postcondition est ce que la fonction garantit après son exécution.
Exemples courants :
Dans le code, une précondition peut être « la liste d’entrées est triée », et la postcondition « la liste de sortie est triée et contient les mêmes éléments plus l’élément inséré ».
Quand vous les notez (même informellement), la conception devient plus nette : vous décidez ce qu’une fonction attend et ce qu’elle promet, et vous la rendez naturellement plus petite et plus ciblée.
En revue, la discussion se déplace du style (« je ferais autrement ») vers la correction (« ce code maintient-il l’invariant ? » « impose‑t‑on la précondition ou la documente‑t‑on ? »).
Vous n’avez pas besoin de preuves formelles pour en tirer profit. Choisissez la boucle la plus boguée ou la mise à jour d’état la plus délicate et ajoutez un invariant d’une ligne au-dessus. Lorsqu’on éditera le code plus tard, ce commentaire servira de garde‑fou : si un changement casse ce fait, le code n’est plus sûr.
Tests et raisonnement visent le même résultat — un logiciel conforme aux attentes — mais ils opèrent différemment. Les tests découvrent les problèmes en essayant des exemples. Le raisonnement prévient des catégories entières de problèmes en rendant la logique explicite et vérifiable.
Les tests sont un filet de sécurité pratique. Ils attrapent des régressions, vérifient des scénarios réels et documentent le comportement attendu de façon exécutable par toute l’équipe.
Mais les tests ne prouvent pas l’absence de bugs. Aucun jeu de tests ne couvre chaque entrée, chaque variation temporelle ou chaque interaction entre fonctionnalités. Beaucoup d’échecs « ça marche sur ma machine » viennent de combinaisons non testées : une entrée rare, un ordre précis d’opérations ou un état subtil n’apparaissant qu’après plusieurs étapes.
Le raisonnement vise à prouver des propriétés du code : « cette boucle termine toujours », « cette variable n’est jamais négative », « cette fonction ne renvoie jamais d’objet invalide ». Bien fait, il exclut des classes entières de défauts — en particulier autour des frontières et des cas limites.
La limite est l’effort et la portée. Des preuves formelles pour un produit entier sont rarement économiquement viables. Le raisonnement est surtout utile de façon sélective : algorithmes centraux, flux sensibles à la sécurité, logique financière et concurrence.
Utilisez les tests largement et appliquez un raisonnement approfondi là où l’échec coûte cher.
Un pont pratique entre les deux est de rendre l’intention exécutable :
Ces techniques ne remplacent pas les tests — elles resserrent le filet. Elles transforment des attentes vagues en règles vérifiables, rendant les bugs plus difficiles à écrire et plus faciles à diagnostiquer.
Le code « malin » semble souvent gagner du temps sur le moment : moins de lignes, un tour astucieux, un one-liner qui flatte l’auteur. Le problème, c’est que l’astuce ne se scale pas dans le temps ni entre les personnes. Six mois plus tard, l’auteur oublie l’astuce. Un nouveau coéquipier la lit littéralement, ignore l’hypothèse cachée et la modifie d’une manière qui casse le comportement. C’est la « dette d’astuce » : de la vitesse à court terme payée par de la confusion à long terme.
Le propos de Dijkstra n’était pas « écrivez du code ennuyeux » par goût — mais que des contraintes disciplinées rendent les programmes plus faciles à raisonner. Sur une équipe, les contraintes réduisent aussi la fatigue décisionnelle. Si tout le monde connaît déjà les choix par défaut (nomenclature, structure des fonctions, règle de gestion des erreurs), on cesse de rejouer les mêmes débats à chaque PR. Ce temps retourne au travail produit.
La discipline se voit dans des pratiques routinières :
Quelques habitudes concrètes préviennent l’accumulation de dette d’astuce :
calculate_total() à do_it()).La discipline n’est pas la perfection — c’est rendre la prochaine modification prédictible.
La modularité n’est pas juste « séparer le code en fichiers ». C’est isoler des décisions derrière des frontières claires, pour que le reste du système n’ait pas à connaître (ou se soucier) des détails internes. Un module cache les parties sales — structures de données, cas limites, optimisations — tout en exposant une petite surface stable.
Quand une demande de changement arrive, l’issue idéale est : un seul module change et tout le reste reste intact. C’est le sens pratique de « garder le changement local ». Les frontières empêchent le couplage accidentel — où la mise à jour d’une fonctionnalité casse silencieusement trois autres parce qu’elles partageaient des hypothèses.
Une bonne frontière rend aussi le raisonnement plus simple. Si vous pouvez énoncer ce qu’un module garantit, vous pouvez raisonner sur le programme plus large sans relire toute son implémentation à chaque fois.
Une interface est une promesse : « Donnez ces entrées, je produirai ces sorties et maintiendrai ces règles. » Quand la promesse est claire, les équipes peuvent travailler en parallèle :
Il ne s’agit pas de bureaucratie — mais de points de coordination sûrs dans une base de code qui grandit.
Pas besoin de revue d’architecture monumentale pour améliorer la modularité. Essayez ces vérifications légères :
Des frontières bien dessinées transforment le « changement » d’un événement système‑large en une modification localisée.
Quand le logiciel est petit, vous pouvez « tout garder en tête ». À l’échelle, ce n’est plus possible — et les modes d’échec deviennent familiers.
Les symptômes courants ressemblent à :
Le pari central de Dijkstra était que les humains sont le goulot. Un flux de contrôle clair, des unités petites et bien définies et du code sur lequel on peut raisonner ne sont pas des choix esthétiques — ce sont des multiplicateurs de capacité.
Dans une grande base de code, la structure agit comme une compression de la compréhension. Si les fonctions ont des entrées/sorties explicites, les modules des frontières nommables et le « happy path » n’est pas mélangé à tous les cas limites, les développeurs passent moins de temps à reconstituer l’intention et plus de temps à faire des changements délibérés.
Quand les équipes grandissent, les coûts de communication augmentent plus vite que le nombre de lignes. Du code discipliné et lisible réduit la quantité de savoir tribal nécessaire pour contribuer en toute sécurité.
Cela se voit immédiatement dans l’onboarding : les nouveaux ingénieurs suivent des patterns prévisibles, apprennent un petit ensemble de conventions et font des modifications sans visite guidée des « pièges ». Le code lui‑même enseigne le système.
Pendant un incident, le temps est rare et la confiance fragile. Le code écrit avec des hypothèses explicites (préconditions), des vérifications significatives et un flux de contrôle simple est plus facile à tracer sous pression.
Tout aussi important, les changements disciplinés sont plus faciles à revenir en arrière. Des éditions plus petites et localisées avec des frontières claires réduisent la probabilité qu’un rollback déclenche de nouvelles défaillances. Le résultat n’est pas la perfection — c’est moins de surprises, une récupération plus rapide et un système qui reste maintenable au fil des années et des contributeurs.
Parce que, quand les bases de code grandissent, le goulot d'étranglement devient la compréhension — pas la saisie. L’accent de Dijkstra sur un flux de contrôle prévisible, des contrats clairs et la correction réduit le risque qu’une « petite modification » provoque des comportements surprenants ailleurs, ce qui ralentit précisément les équipes avec le temps.
Ici, « montée en charge » concerne moins la performance que la complexité qui se multiplie :
Ces forces rendent le raisonnement et la prévisibilité plus précieux que l’astuce ponctuelle.
La programmation structurée favorise un petit ensemble de structures de contrôle claires :
if/else, switch)for, while)Le but n’est pas la rigidité mais de rendre les chemins d’exécution faciles à suivre pour pouvoir expliquer le comportement, relire les changements et déboguer sans « téléportation » dans le code.
Le problème vient des sauts non restreints qui créent des chemins difficiles à prédire et un état obscur. Quand le flux de contrôle est emmêlé, les développeurs perdent du temps à répondre à des questions basiques comme « Comment en sommes-nous arrivés là ? » ou « Quel état est valide maintenant ? »
Les équivalents modernes incluent les branches profondément imbriquées, les sorties multiples dispersées et les changements d’état implicites qui rendent le comportement difficile à tracer.
La correction est la « fonctionnalité silencieuse » sur laquelle les utilisateurs comptent : le système fait de manière constante ce qu’il promet et échoue de façon prévisible et explicable quand il ne peut pas. C’est la différence entre « ça marche pour quelques exemples » et « ça continue de marcher après refactors, intégrations et cas limites ».
Parce que les dépendances amplifient les erreurs. Un mauvais état ou un bug limite se retrouve copié, mis en cache, relancé, encapsulé et „contourné" dans plusieurs modules et services. Avec le temps, les équipes cessent de se demander « qu’est-ce qui est vrai ? » et se contentent de « ce qui arrive généralement », ce qui complique les incidents et rend les changements risqués.
La simplicité, ici, consiste à avoir peu d’idées en mouvement à la fois : responsabilités claires, flux de données limpide et un minimum de cas particuliers. Ce n’est pas moins de lignes ou des one-liners astucieux.
Un bon test : le comportement reste-t-il prévisible quand les exigences changent ? Si chaque nouveau cas ajoute un « sauf si… », vous accumulez de la complexité accidentelle.
Une invariant est un fait qui doit rester vrai pendant l’exécution d’une boucle ou d’une transition d’état. Usage léger :
total est la somme des éléments traités »)Cela rend les modifications ultérieures plus sûres car le prochain lecteur sait ce qui ne doit pas être cassé.
Les tests trouvent des bugs en essayant des exemples ; le raisonnement empêche des catégories entières de bugs en rendant la logique explicite. Les tests ne peuvent pas prouver l’absence de défauts car ils ne couvrent pas tous les inputs ou variations temporelles. Le raisonnement brille surtout là où l’échec coûte cher (argent, sécurité, concurrence).
Un mélange pratique : tests larges + assertions ciblées + préconditions/postconditions claires autour de la logique critique.
Commencez par petits mouvements répétables qui réduisent la charge cognitive :
Ce sont des « améliorations structurelles » incrémentales qui rendent la prochaine modification moins chère sans exiger une réécriture.