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

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.

Architecture logicielle ·
Adel LATIBI
Adel LATIBI

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.

Questions fréquentes

Tell Don't Ask interdit-il complètement les getters ?

Non. Le principe interdit d'utiliser les getters pour reproduire à l'extérieur de l'objet une logique qui devrait vivre à l'intérieur. Les getters restent légitimes pour afficher des données, sérialiser un objet, ou faire des assertions de test. La règle pratique : si tu fais plus de deux getters d'affilée avant un setter, tu écris probablement de la logique qui devrait être une méthode de la classe.

Quelle différence entre Tell Don't Ask et la loi de Déméter ?

Les deux principes sont cousins. La loi de Déméter interdit qu'un objet parle aux amis de ses amis, donc qu'on chaîne des appels du type a.getB().getC().faireQuelqueChose(). Tell Don't Ask est plus large : il dit qu'on ne demande pas à un objet ses données pour décider à sa place. En pratique, respecter TDA fait souvent respecter Déméter automatiquement.

Le principe s'applique-t-il aux DTO et aux objets de transfert ?

Non. Les DTO ont vocation à transporter des données sans comportement. Forcer Tell Don't Ask sur un DTO produit du code absurde. Le principe vise les objets métier qui portent des règles et des invariants. Un DTO, une réponse d'API, une structure de configuration : ces objets exposent légitimement leurs champs.

Comment l'objet doit-il signaler qu'il ne peut pas exécuter une opération ?

Deux options propres : une exception métier explicite (par exemple SoldeInsuffisantException), ou un type de retour qui exprime explicitement l'échec (un Result, un Either, un Optional selon le langage). Ce qui est à éviter : une exception générique sans contexte, ou un retour booléen sans détail sur la cause. L'objet décide, mais il doit aussi communiquer clairement quand il ne peut pas.

Tell Don't Ask est-il compatible avec les architectures hexagonales ou DDD ?

Oui, et c'est même l'un de leurs fondements. Le Domain-Driven Design encourage des entités riches qui encapsulent leurs comportements et leurs invariants, exactement ce que prescrit Tell Don't Ask. Les architectures hexagonales reposent sur des objets de domaine actifs qui n'ont pas besoin d'être pilotés par leur infrastructure pour décider.

Comment introduire ce principe dans une équipe qui n'y est pas habituée ?

Pas par décret. Le mieux est de l'introduire en revue de code, sur des cas concrets : montrer un bloc qui enchaîne getters et setters, proposer la version refactorée avec une méthode de comportement, mesurer la différence en lisibilité et en testabilité. Les équipes adoptent un principe quand elles voient son bénéfice sur leur propre code, pas quand on leur impose une règle abstraite.

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