Monde pur / impur : isoler le métier du chaos
Quand on développe une feature, on peut facilement commencer à mélanger deux choses très différentes :
- le monde réel : BDD, API HTTP, logs, fichiers, réseau
- la logique métier : une transformation de données
On peut déplacer ce code dans un service, créer un controller plus propre, ajouter un repository, ou injecter les dépendances correctement. C'est déjà mieux qu'un gros bloc dans une route HTTP.
Mais ce n'est pas toujours suffisant. Un service peut lui aussi devenir un endroit où l'on mélange orchestration, accès aux données, appels externes et règles métier.
Le vrai sujet, c'est de savoir si la règle métier peut être comprise et testée sans traîner toute l'infra avec elle.
Dans ce mini POC, le scénario est volontairement petit :
Calculer un prix TTC à partir d'un prix HT et d'un taux de TVA.
La règle métier est donc :
prixTtc = prixHt * (1 + tauxTva)
Version classique : tout mélangé
Dans la version classique, une seule fonction fait tout :
const calculerPrixFinal = async (productId: string) => {
const product = await db.getProduct(productId);
const response = await fetch("https://httpbin.org/json");
const tva = await response.json();
const tauxTva = Number(tva.slideshow?.slides?.length ?? 2) / 10;
const prixTtc = product.prixHt * (1 + tauxTva);
await logger.info(`Prix calculé pour ${productId}`);
return prixTtc;
};
Sur le papier, ça marche.
Mais si je veux vérifier une chose très simple :
Est-ce que 100 € HT avec 20 % de TVA donne 120 € TTC ?
Je ne peux pas poser directement cette question au code.
Je suis obligé de passer par :
- un
productId - une fausse base de données
- un appel HTTP
- le format de réponse de l'API
- un logger
- de l'asynchrone
Donc pour tester une multiplication, je dois simuler le monde réel.
Je pense que vous commencez à voir le vrai problème.
classic : tester une règle = simuler l'infra
On ne teste plus seulement une règle métier. On teste aussi tout ce qui l'entoure.
Et c'est précisément là que le code devient fragile : la règle métier est prisonnière du chaos.
Dans une démarche de Software craftsmanship, une des premières habitudes à prendre est justement de repérer ce qui doit rester simple, stable et facile à tester. C'est là qu'arrive l'idée de séparer le monde pur du monde impur.
Version clean : isoler le cœur pur
Revenons à notre objectif de départ.
Nous voulons surtout nous assurer que cette règle est correcte :
Calculer un prix TTC à partir d'un prix HT et d'un taux de TVA.
Peu importe que le produit vienne d'une BDD, que le taux de TVA vienne d'une API HTTP, ou qu'un logger écrive quelque chose à la fin. Ces éléments appartiennent au monde réel, donc au monde impur.
Mais la règle métier, elle, doit pouvoir être vérifiée sans tout ce bruit autour.
La correction ne consiste donc pas à supprimer l'infra.
La correction consiste à sortir la transformation métier dans une fonction pure :
const calculerPrixTtc = (prixHt: number, tauxTva: number) => {
return prixHt * (1 + tauxTva);
};
Cette fonction ne connaît pas :
- la BDD
- HTTP
- les logs
- Express
- Bun
Elle reçoit des données. Elle retourne des données.
INPUT -> TRANSFORMATION -> OUTPUT
Maintenant, le test métier devient direct :
console.log(calculerPrixTtc(100, 0.2) === 120);
Plus besoin de mocker fetch.
Plus besoin de charger un produit.
Plus besoin de logger quoi que ce soit.
Dans le code, cette séparation donne ça :
// Fonction pure :
// mêmes entrées => même sortie, aucun accès BDD, aucun HTTP, aucun log.
const calculerPrixTtc = (prixHt: number, tauxTva: number) => {
return prixHt * (1 + tauxTva);
};
// Fonction impure :
// elle orchestre le monde réel autour du cœur pur.
const calculerPrixFinal = async (productId: string) => {
const product = await db.getProduct(productId);
const response = await fetch("https://httpbin.org/json");
const tva = await response.json();
const tauxTva = Number(tva.slideshow?.slides?.length ?? 2) / 10;
const prixTtc = calculerPrixTtc(product.prixHt, tauxTva);
await logger.info(`Prix calculé pour ${productId}`);
return prixTtc;
};
Important : calculerPrixFinal n'est pas pure.
Elle est seulement mieux organisée, parce que son calcul métier est délégué à
calculerPrixTtc.
Un peu de théorie
Vous avez vu l'avantage sur un petit exemple : le calcul devient plus simple à tester, plus simple à lire et plus simple à faire évoluer.
Maintenant, mettons des mots sur le concept.
Une fonction pure est une fonction qui respecte deux idées :
- pour les mêmes entrées, elle retourne toujours la même sortie
- elle ne produit pas d'effet de bord
Un effet de bord, c'est une interaction avec le monde extérieur :
- lire ou écrire en base de données
- appeler une API HTTP
- écrire un log
- lire l'heure actuelle
- générer un nombre aléatoire
- modifier une variable globale
Donc cette fonction est pure :
const calculerPrixTtc = (prixHt: number, tauxTva: number) => {
return prixHt * (1 + tauxTva);
};
Mais cette fonction ne l'est pas :
const calculerPrixTtc = (prixHt: number, tauxTva: number) => {
const prixTtc = prixHt * (1 + tauxTva);
console.log(`Prix TTC calculé : ${prixTtc}`);
return prixTtc;
};
Et ce n'est pas grave.
Dans une vraie application, tout ne peut pas être pur. Une application doit parler à une BDD, recevoir des requêtes HTTP, appeler des services externes et écrire des logs.
L'objectif n'est donc pas de supprimer le monde impur.
L'objectif est de mettre le monde impur autour du cœur métier :
infra -> métier pur -> infra
Dans notre exemple :
charger produit + récupérer TVA -> calculerPrixTtc -> logger / retourner
Le cœur important devient petit :
calculerPrixTtc(prixHt, tauxTva)
C'est ce petit cœur qui porte la règle.
Le reste orchestre le monde réel autour de lui.
C'est souvent l'une des premières notions à intégrer pour aborder sérieusement l'architecture logicielle.
En architecture applicative, certains frameworks imposent déjà une partie de cette discipline. Par exemple, dans un framework assez structuré comme Symfony, on retrouve vite des notions de controllers, services, repositories, validation, dependency injection, etc. Ces conventions ne rendent pas le code pur automatiquement, mais elles évitent souvent de tout poser au même endroit.
Dans l'écosystème JavaScript, on a souvent plus de liberté. C'est puissant, mais ça veut aussi dire qu'on peut très vite mélanger route HTTP, accès BDD, appel API et logique métier dans une seule fonction si on ne pose pas nos propres frontières.
Un bon signal d'alerte : si écrire un test unitaire ou faire du TDD devient
pénible, ce n'est pas toujours le test le problème. C'est parfois le signe que
le code n'isole pas assez sa partie pure. Quand une règle métier demande de
mocker une BDD, un fetch, un logger et trois détails de framework, c'est
souvent que la règle est trop collée au monde réel.
Conclusion et les LLM
Le réflexe à avoir quand on développe une fonctionnalité, ce n'est pas seulement de penser :
Crée-moi une fonction qui fait ça.
Avec un monde informatique de plus en plus généré avec des LLM, ce réflexe devient encore plus important. Si on demande une fonction trop vague, l'IA risque de mélanger l'infra, les appels externes, les logs et la règle métier au même endroit. Résultat : plus de tokens consommés, plus d'allers-retours, et souvent plus de frustration.
La demande utile est souvent plus précise :
Crée-moi une fonction pure pour cette logique métier, puis branche-la à cette feature.
Autrement dit, il faut prendre quelques secondes pour repérer la règle importante : quelle transformation métier est cachée dans cette fonctionnalité ?
Une fois cette règle trouvée, l'objectif devient plus clair : séparer la transformation métier du monde réel.
On peut même transformer ce réflexe en consigne réutilisable dans un Skill : demander à l'agent d'identifier le cœur métier pur avant de générer le code d'orchestration. Pour creuser cette idée, j'en parle dans l'article Comprendre les Skills.
Le but est de repérer la petite règle qui doit rester simple, testable et prédictible, puis de laisser l'infrastructure tourner autour d'elle.