Aller au contenu principal
Lance-toi : 40% de réduction sur toutes les formations jusqu'au 30 juin

Principe de moindre surprise : pourquoi le bon code est celui qui ne vous surprend jamais

Une fonction qui s'appelle getUser() ne devrait pas créer un utilisateur quand il n'existe pas. Un bouton "Annuler" ne devrait pas valider la modification. Ces situations existent partout, et elles violent toutes le même principe.

Architecture logicielle ·
Adel LATIBI
Adel LATIBI
Principe de moindre surprise : pourquoi le bon code est celui qui ne vous surprend jamais

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

Un développeur ouvre un fichier qu'il ne connaît pas. Il voit une fonction nommée getUserById(id). Il l'utilise dans son code, persuadé de comprendre ce qu'elle fait. Trois jours plus tard, le support remonte un bug : des utilisateurs apparaissent en base avec des données vides. Investigation. La fonction getUserById ne fait pas que récupérer un utilisateur. Si l'ID n'existe pas, elle en crée un automatiquement, vide, avec cet ID.

Le développeur n'a rien fait de mal. Il a fait confiance au nom de la fonction. C'est la fonction qui a trahi sa promesse. Cette situation a un nom : violation du Principle of Least Astonishment, ou principe de moindre surprise en français. Il dit quelque chose de simple : votre code doit faire ce que son nom annonce, et rien d'autre.

Ce principe est moins formalisé que SOLID ou DRY. On ne le trouve pas dans les manuels d'algorithmique. Il est pourtant à l'origine d'une grande partie des bugs qui cassent la confiance dans une base de code.

Le vrai problème : la confiance silencieuse

Quand un développeur lit du code, il fait des dizaines d'hypothèses chaque minute sans en avoir conscience. Il suppose qu'une fonction isValid() retourne un booléen et n'a pas d'effet de bord. Il suppose qu'un getter ne modifie pas l'état. Il suppose qu'une méthode statique n'écrit pas en base. Ces hypothèses ne sont jamais vérifiées explicitement, parce qu'elles sont fondées sur une convention implicite partagée par toute la communauté du développement.

Quand votre code respecte ces conventions, le travail des autres développeurs devient fluide. Quand il les viole, chaque ligne devient suspecte. Plus aucune hypothèse n'est sûre. Le coût cognitif explose, parce qu'il faut désormais lire chaque fonction en détail au lieu de faire confiance à son nom.

"Le composant d'un système doit se comporter d'une manière cohérente avec ce que les utilisateurs attendent." Formulation classique du principe

L'origine du principe vient de la conception d'interface utilisateur. On l'attribue souvent au manuel de PL/I dans les années 1970, qui mentionnait que "tout aspect d'une langue qui cause un étonnement chez l'utilisateur doit être considéré comme problématique". Le principe a été repris dans le développement logiciel parce qu'il s'applique à toutes les frontières où un humain rencontre un système : interfaces graphiques, API, librairies, lignes de commande, configuration.

Les violations classiques en code

Le getter qui n'est pas un getter

L'une des violations les plus fréquentes. Un développeur lit ce nom :

user.getProfil();

Il s'attend à recevoir le profil de l'utilisateur. Si getProfil() incrémente un compteur de visites, déclenche un log, met à jour la dernière date d'accès en base, ou pire, crée un profil vide quand il n'existe pas, on viole le principe. Le nom annonce une lecture, le code fait une écriture.

La règle simple : un nom qui commence par get, find, is, has, can ne doit avoir aucun effet de bord observable. Si la méthode modifie quelque chose, son nom doit le refléter : getOrCreateProfil(), fetchProfilWithTracking(), etc.

Les paramètres booléens magiques

Un grand classique :

commande.envoyer(true);
commande.envoyer(false);

Que signifie true ici ? Envoyer en express ? Envoyer une copie ? Envoyer même si le stock est à zéro ? Le lecteur n'a aucun moyen de le savoir sans aller voir la signature. Pire, deux développeurs peuvent interpréter différemment le booléen et introduire des bugs subtils.

Mieux : un argument nommé ou un objet d'options.

commande.envoyer({ enExpress: true });

Les valeurs par défaut surprenantes

Une fonction qui accepte un timeout avec une valeur par défaut de 30 secondes est explicite. Une fonction où la valeur par défaut est cachée dans un fichier de configuration global, ou pire, dépend d'une variable d'environnement, viole le principe. Le développeur qui appelle la fonction n'a aucun moyen de prédire le comportement sans aller fouiller ailleurs.

Les exceptions silencieuses ou capturées trop large

Un bloc try / catch qui attrape toutes les exceptions et les ignore, ou qui les transforme en valeur de retour null, est une violation grave. L'appelant pense que la fonction a réussi alors qu'elle a échoué. Les bugs qui en résultent sont parmi les plus difficiles à diagnostiquer parce qu'ils n'apparaissent jamais dans les logs.

La méthode qui retourne plusieurs choses différentes selon le contexte

Une fonction qui retourne un objet en cas de succès, un booléen en cas de doublon, et null en cas d'erreur est un piège. L'appelant doit deviner le type de retour à chaque appel. Mieux vaut lever une exception en cas d'erreur et retourner un type stable en cas de succès.

Le principe appliqué aux API publiques

Une API REST ou GraphQL est une frontière où le principe de moindre surprise prend une importance cruciale. Chaque incohérence entre endpoints crée un coût pour tous les développeurs qui consomment l'API.

Si GET /utilisateurs/42 retourne un objet utilisateur et GET /commandes/100 retourne un tableau avec un seul élément, le consommateur perd son temps à comprendre cette incohérence. Si DELETE /utilisateurs/42 retourne 204 et DELETE /commandes/100 retourne 200 avec un body, c'est une charge cognitive supplémentaire pour rien.

Les conventions REST existent justement pour réduire ces surprises. Les codes HTTP standards, les structures de réponses cohérentes, les conventions de nommage des endpoints au pluriel, les erreurs avec un format unifié : tout cela vise à ce qu'un développeur qui n'a jamais utilisé votre API puisse en deviner 80% du comportement par analogie avec d'autres APIs qu'il connaît.

Le même principe s'applique côté frontend. Un composant React appelé <UserCard /> qui modifie l'utilisateur en base quand on le rend, c'est une catastrophe pédagogique. Les formations React et JavaScript avancé insistent sur cette discipline : un composant qui affiche, un hook qui agit, et la frontière entre les deux qui reste lisible.

Un cas concret : refactoriser une fonction surprenante

Voici une fonction inspirée du vrai monde, qui a accumulé plusieurs surprises au fil du temps :

function getUser(id, options) {
  if (!id) return null; // surprise 1 : pas d'id, pas d'erreur
  let user = db.users.find(id);
  if (!user) {
    user = db.users.create({ id, nom: '' }); // surprise 2 : create silencieux
  }
  user.derniereVisite = new Date();
  db.users.save(user); // surprise 3 : effet de bord à l'écriture
  if (options && options.format) {
    return formatUser(user, options.format); // surprise 4 : retour variable
  }
  return user;
}

Cette fonction a quatre comportements cachés. Aucun ne saute aux yeux quand on lit le nom. La version respectueuse du principe la décompose en plusieurs fonctions au nom explicite :

function getUser(id) {
  if (!id) throw new Error('id requis');
  return db.users.find(id) ?? null;
}

function getOrCreateUser(id) {
  if (!id) throw new Error('id requis');
  return db.users.find(id) ?? db.users.create({ id, nom: '' });
}

function trackUserVisit(user) {
  user.derniereVisite = new Date();
  db.users.save(user);
}

function formatUserForDisplay(user, format) {
  return formatUser(user, format);
}

Quatre fonctions au lieu d'une, mais chacune fait ce que son nom annonce, sans surprise. Le développeur appelant choisit explicitement la combinaison qu'il veut. La lecture devient prévisible, les bugs deviennent rares.

Le principe dans les conventions et les frameworks

Une grande partie de la valeur des frameworks modernes vient de leur respect du principe de moindre surprise au sein de leur écosystème. Symfony, par exemple, applique des conventions strictes sur le nommage des contrôleurs, des services, des repositories. Quand vous arrivez sur un projet Symfony 7 que vous n'avez jamais vu, vous savez où chercher chaque chose. Cette prévisibilité est un actif énorme pour la productivité d'équipe. La formation Symfony 7 et Spring Boot mettent ces conventions au cœur de leur approche.

De même, Next.js a fait des choix très forts sur le routage par fichiers : un fichier app/produits/[id]/page.tsx est forcément la page d'un produit avec un ID dynamique. Aucune surprise possible. Cette régularité réduit drastiquement la courbe d'apprentissage et permet aux nouveaux développeurs d'être productifs en jours plutôt qu'en semaines. Cette logique est explorée en profondeur dans la formation Next.js.

Les signaux qui révèlent une violation

Les commentaires qui commencent par "attention"

Quand un commentaire dit "attention, cette fonction modifie aussi X" ou "ne pas oublier que cette méthode fait Y en arrière-plan", c'est l'aveu d'une violation. Le code devrait pouvoir se passer de ces avertissements. Si le nom était bon et que le comportement était cohérent, le commentaire ne serait pas nécessaire.

Les bugs qui commencent par "je pensais que..."

Quand un développeur explique un bug en disant "je pensais que cette fonction faisait juste X", c'est presque toujours qu'elle faisait X plus quelque chose d'autre. Ces bugs ne sont pas des erreurs du développeur, ils sont des erreurs de conception du code qu'il a utilisé.

Le besoin de lire la documentation pour chaque utilisation

Une bonne API peut être utilisée par auto-complétion. Quand chaque appel oblige à ouvrir la doc pour vérifier les effets cachés, c'est que l'API surprend en permanence. La documentation devrait être un complément, pas une obligation.

Les méthodes longues à plusieurs comportements internes

Une fonction de cent lignes qui valide, calcule, persiste, notifie et logue est par définition surprenante. Le nom ne peut pas couvrir tout ça. La séparation des préoccupations, abordée dans l'article dédié, est l'antidote direct à ce genre de violation.

Comment cultiver le réflexe de moindre surprise

Comme tous les principes de qualité de code, celui-ci s'acquiert par exposition et pratique. Quelques habitudes accélèrent l'apprentissage.

Faites le test du nom. Avant d'écrire une fonction, demandez-vous quel nom décrirait son comportement de manière exhaustive. Si vous avez besoin d'un "et" ou d'un "puis" dans le nom, c'est qu'elle fait deux choses. Découpez-la.

Lisez votre code comme si vous ne le connaissiez pas. Le lendemain matin, ouvrez le code que vous avez écrit la veille et lisez-le sans contexte. Notez chaque endroit où vous êtes obligé de remonter dans la définition pour comprendre ce qui se passe. Chaque remontée est un signal de surprise potentielle.

Écoutez les questions des collègues. Quand un membre de l'équipe vous demande "est-ce que cette fonction fait aussi X", votre première réaction ne doit pas être de répondre. Elle doit être de noter que la fonction n'est pas évidente. La réponse devrait être visible directement.

Suivez les conventions de votre langage et de votre framework. Pythoniser un projet Python (snake_case, gestionnaires de contexte, dunder methods), respecter les conventions Symfony, suivre les patterns React idiomatiques : tout cela réduit la surprise pour les futurs lecteurs.

Le lien avec les autres principes

Le principe de moindre surprise renforce et est renforcé par presque tous les autres principes de code.

Il prolonge naturellement KISS : un code simple est par définition moins surprenant qu'un code complexe avec des comportements cachés. Le simple ne ment pas, le complexe peut.

Il s'articule avec Separation of Concerns et le SRP de SOLID : une fonction qui fait une seule chose est facile à nommer correctement, donc moins surprenante.

Il complète YAGNI : un code qui n'anticipe pas des cas hypothétiques est plus prévisible qu'un code chargé d'options et de comportements conditionnels.

Il pousse à respecter la Law of Demeter : si un objet ne traverse pas ses voisins pour aller chercher des données dans des couches lointaines, son comportement reste local et donc prévisible.

Ce que ce principe apporte vraiment

Les bénéfices d'un code respectueux de la moindre surprise se mesurent surtout sur la durée. À court terme, l'effort initial peut sembler excessif (nommer précisément, séparer les comportements, expliciter les options). À moyen terme, les gains s'accumulent.

L'onboarding accélère. Un nouveau développeur peut contribuer en jours plutôt qu'en semaines, parce que chaque fonction qu'il rencontre fait ce qu'elle promet.

Les bugs liés aux malentendus disparaissent. La catégorie entière des "j'ai cru que cette fonction faisait juste X" s'éteint quand chaque fonction est honnête sur ses effets.

La revue de code devient plus rapide. Les reviewers n'ont plus besoin de vérifier chaque appel pour s'assurer qu'il fait bien ce qu'on en attend. La confiance dans le code monte d'un cran.

Le refactoring devient plus sûr. Un code dont chaque fonction est prévisible peut être réorganisé sans craindre des effets de bord cachés. Les tests sont plus faciles à écrire parce que le comportement attendu est clair.

FAQ

Le Principle of Least Astonishment est-il subjectif ?

Partiellement. Ce qui surprend un développeur Python expérimenté ne surprendra pas un développeur Java. Le principe est donc relatif à un public et à un contexte. La règle pratique : "moins de surprise pour qui ?". Si votre code est utilisé par une équipe Python, alignez-vous sur les conventions Python. Si votre API est consommée par des développeurs habitués à REST, respectez les conventions REST. Le principe n'est pas absolu, il est contextuel.

Comment faire la différence entre une vraie surprise et une nouvelle abstraction utile ?

Une nouvelle abstraction utile est une simplification d'un concept connu, pas une rupture avec les conventions. Si vous introduisez quelque chose de nouveau, le test est : un développeur compétent peut-il comprendre votre choix en lisant le nom et en regardant l'utilisation ? Si oui, c'est une bonne abstraction. Si non, c'est une surprise déguisée. Documenter une "originalité" est rarement la solution. Mieux vaut renommer ou redécouper.

Ce principe s'applique-t-il aux noms de variables ?

Oui, complètement. Une variable nommée data qui contient une liste, ou utilisateur qui contient un identifiant, surprend chaque lecteur. Les noms doivent refléter le type et le rôle de la valeur. La règle générale : un développeur qui lit le nom doit pouvoir prédire ce qu'il y a dedans. Quand ce n'est pas le cas, le nom est mauvais.

Faut-il documenter les exceptions levées par une fonction pour respecter le principe ?

Oui, dans la plupart des cas. Une fonction qui peut lever des exceptions doit l'indiquer dans sa signature ou sa documentation. Les langages comme Java rendent cela obligatoire avec les checked exceptions. Dans les langages qui ne l'imposent pas (JavaScript, Python, PHP), c'est au développeur de tenir cette discipline. Une exception non documentée qui surgit est l'archétype de la surprise.

Ce principe s'applique-t-il aussi aux interfaces utilisateur ?

C'est même son origine. Le Principle of Least Astonishment vient du domaine de l'interface utilisateur. Un bouton "Annuler" qui valide, une icône poubelle qui archive au lieu de supprimer, un raccourci clavier qui change selon le contexte sans signal visuel : tous violent le principe. La cohérence des interactions est l'un des piliers de l'UX, et son lien avec la conception d'API est très direct. Une API est une interface utilisateur pour développeurs.

Comment refactoriser une fonction qui surprend, sans casser les appelants existants ?

La technique éprouvée est l'introduction progressive. Créez la nouvelle fonction au nom clair à côté de l'ancienne, marquez l'ancienne comme dépréciée avec un avertissement, puis migrez les appelants un par un. Une fois que tous les appels utilisent la nouvelle version, supprimez l'ancienne. Cette approche est compatible avec le travail en équipe et évite les big bang refactorings risqués. Les formations PHP orienté objet et Python POO abordent ces stratégies de refactoring sur des cas concrets.

Existe-t-il des outils qui détectent les violations de ce principe ?

Pas directement, parce que la surprise est par nature contextuelle et difficile à formaliser. Cependant, plusieurs outils détectent des symptômes : ESLint signale les fonctions trop longues, SonarQube identifie les complexités cyclomatiques élevées, les linters Python (Pylint, Ruff) repèrent les noms non conformes aux conventions. Ces outils ne remplacent pas la revue humaine, mais ils éliminent les violations les plus mécaniques.

Le principe est-il compatible avec des paradigmes comme la programmation fonctionnelle ?

Oui, et même particulièrement bien. La programmation fonctionnelle pure interdit par construction la plupart des sources de surprise : pas d'effets de bord, pas de mutation, pas d'état caché. Une fonction pure est par essence respectueuse du principe : son comportement est entièrement déterminé par ses entrées. C'est l'une des raisons pour lesquelles les paradigmes fonctionnels ont gagné en popularité dans le développement applicatif moderne, y compris en JavaScript et en Python.

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous