Tell Don't Ask : pourquoi votre code passe sa vie à interroger des objets au lieu de leur parler
Tu écris une fonction, tu récupères trois champs d'un objet, tu fais un calcul, tu remets le résultat dans l'objet. Tu refais la même chose la semaine suivante avec un autre objet. Ce schéma a un nom, il a aussi un antidote.
Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.
La programmation orientée objet existe depuis cinquante ans. Et pourtant, la majorité du code écrit en POO ressemble à du code procédural déguisé. Des objets qui ne servent qu'à stocker des données. Des fonctions extérieures qui font tout le travail. Des getters partout, des setters partout, et aucune méthode qui exprime ce que l'objet sait faire.
Toi, en reconversion ou junior, tu apprends la POO avec des cours qui te montrent comment créer une classe, ajouter des attributs privés, générer des getters et des setters. C'est techniquement de la POO. C'est philosophiquement du procédural avec des étapes en plus.
Le blocage n'est pas la syntaxe. Le blocage est dans la manière de penser. Tant que tu écris du code qui pose des questions à tes objets, tu rates ce qui fait la valeur de la POO. Le principe Tell Don't Ask, abrégé TDA, est l'idée la plus simple pour basculer dans l'autre mode.
Cet article te montre où ça coince, ce que le principe veut dire, comment l'appliquer sans excès, et où s'arrête sa zone d'utilité.
Le problème : ton code interroge tes objets au lieu de les utiliser
Voilà un code qu'on retrouve dans tous les projets, junior comme senior, peu importe le langage.
if (commande.getStatut() === 'EN_ATTENTE'
&& commande.getDateCreation() < dateLimite
&& commande.getMontant() > 0
&& commande.getClient().getEstActif() === true) {
commande.setStatut('CONFIRMEE');
commande.setDateConfirmation(new Date());
commande.setNumeroTransaction(genererNumero());
}
Lis ce bloc et réponds à une question : où vit la logique de confirmation d'une commande ? Réponse : nulle part. Elle est éparpillée. Le code appelant connaît le statut interne. Il connaît la règle métier sur la date. Il connaît la dépendance au client. Il décide du nouveau statut. Il appelle trois setters d'affilée pour mettre l'objet dans un état cohérent.
L'objet Commande est passif. Il subit. Il est interrogé, modifié, reposé sur l'étagère. Toute la logique qui le concerne vit ailleurs, dans des services, des contrôleurs, des helpers.
Ce style a quatre conséquences que tu vas finir par croiser :
- La même règle de confirmation se retrouve dupliquée dans trois fichiers, parce que trois endroits du code doivent confirmer une commande.
- Le jour où la règle change (ajout d'une vérification de stock par exemple), tu dois retrouver les trois endroits, et tu en oublies toujours un.
- Les tests deviennent pénibles, parce que pour tester la confirmation, il faut simuler tout le contexte d'appel.
- L'objet Commande ne raconte plus l'histoire de ce qu'est une commande. Il est devenu un sac de données.
Le principe Tell Don't Ask vise exactement ce problème.
Le principe : dis à l'objet quoi faire, ne lui demande pas son état
Le principe Tell Don't Ask a été formulé par Andy Hunt et Dave Thomas dans les années 2000, repris ensuite par Martin Fowler et la communauté Pragmatic Programmer. L'idée tient en une phrase : un objet doit exposer des opérations qui expriment ce qu'il fait, pas des accesseurs qui exposent ce qu'il contient.
Au lieu de demander à l'objet ses données puis de décider à sa place, tu lui dis directement ce que tu veux qu'il fasse, et tu le laisses gérer ses règles internes.
Le code précédent, écrit dans cet esprit, devient :
commande.confirmer(dateLimite);
Et toute la logique précédente vit à l'intérieur de la méthode confirmer de la classe Commande. La vérification du statut actuel, la comparaison avec la date limite, la validation du client actif, la mise à jour du statut, l'horodatage, la génération du numéro de transaction. Tout. Au même endroit.
L'appelant ne sait plus rien des conditions internes. Il sait juste qu'il veut confirmer une commande. Si la commande ne peut pas être confirmée, elle lève une exception ou retourne un résultat explicite. C'est elle qui décide, parce que c'est elle qui contient les règles.
Ce principe est cousin direct de la loi de Déméter, qui dit en gros la même chose sous un angle différent : ne parle pas aux amis de tes amis. Quand tu fais commande.getClient().getEstActif(), tu violes les deux principes en même temps. Tu interroges la commande, qui interroge le client, et tu prends une décision à leur place.
Trois exemples qui rendent le principe clair
Exemple 1 : un compte bancaire qui se défend tout seul
Style "Ask", le plus courant chez les débutants :
if (compte.getSolde() >= montant && !compte.getBloque()) {
compte.setSolde(compte.getSolde() - montant);
compte.setDerniereOperation(new Date());
}
Style "Tell", l'objet protège ses propres invariants :
compte.debiter(montant);
À l'intérieur de débiter, la classe vérifie le solde, le statut du compte, applique le débit, met à jour la date. Si elle ne peut pas, elle lève une SoldeInsuffisantException ou retourne un échec explicite. L'invariant "un compte ne peut jamais être négatif sauf avec autorisation de découvert" est protégé par la classe, pas par celui qui l'appelle.
Exemple 2 : un panier qui calcule sa propre remise
Version Ask :
let total = 0;
for (const article of panier.getArticles()) {
total += article.getPrix() * article.getQuantite();
}
if (panier.getClient().getNiveau() === 'PREMIUM') {
total = total * 0.9;
}
panier.setTotal(total);
Version Tell :
const total = panier.calculerTotal();
Le panier sait additionner ses articles. Il sait qu'il a un client. Il sait appliquer la remise correspondant au niveau. La règle de calcul vit dans la classe Panier, pas dans le code appelant. Si demain la remise Premium passe à 15%, tu modifies une seule ligne dans une seule méthode.
Exemple 3 : un utilisateur qui répond aux questions sur ses droits
Au lieu d'écrire dans chaque contrôleur :
if (utilisateur.getRole() === 'ADMIN'
|| (utilisateur.getRole() === 'EDITEUR'
&& utilisateur.getEquipe() === article.getEquipe())) {
// autoriser modification
}
Tu écris :
if (utilisateur.peutModifier(article)) {
// autoriser modification
}
La méthode peutModifier encapsule la règle, et elle peut évoluer sans que tu touches à un seul contrôleur. C'est ici qu'on voit que TDA n'élimine pas toujours les getters, il les remplace souvent par des méthodes qui répondent à une question métier plutôt qu'à une question technique. La nuance est importante.
Les pièges à éviter quand tu commences à appliquer TDA
Piège 1 : supprimer tous les getters
Le principe ne dit pas "interdis les getters". Il dit "ne te sers pas des getters pour reproduire à l'extérieur une logique qui devrait être à l'intérieur". Tu auras toujours besoin de getters pour afficher des données dans une interface, sérialiser un objet en JSON, ou faire des assertions dans tes tests. Le problème, c'est l'usage, pas l'existence.
Piège 2 : appliquer TDA aux objets qui n'ont pas de comportement
Tous les objets ne sont pas des objets métier. Les DTO (Data Transfer Object), les entités utilisées comme structures de données pour la sérialisation, les value objects simples, ces classes ont vocation à exposer leurs champs. Forcer TDA sur un DTO produit du code absurde. Le principe vise les objets qui portent du comportement et des règles, pas les structures de transport.
Piège 3 : créer des objets dieux
Si tu pousses TDA sans réfléchir, ta classe Commande va finir avec quarante méthodes : confirmer, annuler, expédier, rembourser, archiver, exporter, notifier, etc. Tu remplaces le code procédural extérieur par une classe obèse intérieure. Ça reste mauvais. TDA fonctionne main dans la main avec la séparation des préoccupations : chaque objet ne porte que les comportements qui lui appartiennent réellement. Les opérations transverses (envoi d'email, journalisation, intégration externe) vivent dans d'autres objets, et la commande les appelle ou est notifiée par eux.
Piège 4 : casser l'encapsulation par les exceptions
Quand l'objet décide tout seul et qu'il ne peut pas exécuter une opération, il doit communiquer l'échec proprement. Une exception métier explicite, ou un type de retour qui exprime l'échec, c'est correct. Une exception générique qui force l'appelant à deviner ce qui s'est passé, c'est pire que le code Ask de départ. Ce point recoupe ce qu'on a vu sur Fail Fast : échouer tôt, oui, mais en disant pourquoi.
Piège 5 : oublier que TDA est un guide, pas une loi
Certains contextes échappent au principe. Une couche de requêtes en lecture, un rapport qui agrège des données de cinquante objets, une interface qui doit afficher dix champs d'une entité. Dans ces cas, demander les données est l'opération elle-même. Vouloir absolument tout cacher derrière des méthodes de comportement complique le code sans bénéfice. Le principe est un outil, il sert quand il aide.
Comment t'entraîner à penser Tell Don't Ask
L'application du principe se travaille par observation, pas par règle apprise. Voici trois exercices à intégrer dans ta routine de code.
Le test du nom de méthode. Quand tu écris une méthode publique sur une classe, demande-toi si son nom raconte une intention métier (confirmer, débiter, valider, expédier) ou une opération technique (getXxx, setXxx, isXxx). Si ta classe ne contient que des verbes techniques, elle est probablement passive.
Le test du chaînage. Compte combien de points tu as dans une seule expression. objet.methode().autre().valeur(). Trois points et plus, c'est presque toujours une violation simultanée de TDA et de la loi de Déméter. Ce code peut souvent être refactoré en une seule méthode bien nommée sur le premier objet.
Le test du déplacement. Repère un bloc d'une dizaine de lignes qui manipule un seul objet avec des getters et setters. Essaie de déplacer ce bloc dans une nouvelle méthode de la classe concernée. Si le déplacement rend le code plus clair, tu viens d'appliquer TDA. Si ça rend la classe incohérente, tu as touché à un cas où le principe ne s'applique pas.
Ces réflexes ne s'attrapent pas en lisant. Ils s'attrapent en écrivant, en lisant le code des autres, et en relisant son propre code six mois plus tard. C'est tout l'intérêt de pratiquer la POO sur des projets concrets, comme on le fait dans les formations PHP orienté objet ou les fondamentaux de Java et de la POO.
Ce que TDA change dans ta manière de concevoir une application
Quand TDA devient un réflexe, ton style de code change en profondeur, pas juste à la marge.
Les contrôleurs deviennent maigres. Une route qui faisait soixante lignes en fait quinze, parce qu'elle se contente d'appeler des méthodes métier. Les services centralisés deviennent moins nécessaires, parce que la logique vit là où vit la donnée. Les tests unitaires sur les classes métier deviennent plus naturels, parce que tu testes un comportement complet plutôt qu'une suite d'appels.
Et surtout, ton code raconte une histoire lisible. La classe Commande a des méthodes qui décrivent le cycle de vie d'une commande. La classe Panier a des méthodes qui décrivent les opérations sur un panier. Un nouveau venu dans l'équipe ouvre une classe et comprend ce qu'elle sait faire, sans avoir à suivre dix appels à travers vingt fichiers.
Le principe Tell Don't Ask est l'un des plus puissants pour qui veut sortir du procédural-déguisé-en-POO. Il s'apprend en pratiquant, il s'oublie en cliquant trop vite sur "générer getters et setters" dans l'IDE. À chaque fois que tu écris un nouveau code, pose-toi la question : est-ce que je suis en train de demander à cet objet de me donner ses données pour décider à sa place, ou est-ce que je lui dis ce qu'il doit faire ? La réponse oriente le reste.