Comment choisir entre Tell Don't Ask et Command Query Separation?

25

Le principe Tell Don't Ask dit:

vous devez essayer de dire aux objets ce que vous voulez qu'ils fassent; ne leur posez pas de questions sur leur état, ne prenez pas de décision, puis dites-leur quoi faire.

Le problème est que, en tant qu'appelant, vous ne devez pas prendre de décisions basées sur l'état de l'objet appelé, ce qui vous amène à changer ensuite l'état de l'objet. La logique que vous implémentez est probablement la responsabilité de l'objet appelé, pas la vôtre. Pour vous, prendre des décisions en dehors de l'objet viole son encapsulation.

Un exemple simple de «Dites, ne demandez pas» est

Widget w = ...;
if (w.getParent() != null) {
  Panel parent = w.getParent();
  parent.remove(w);
}

et la version tell est ...

Widget w = ...;
w.removeFromParent();

Mais que faire si j'ai besoin de connaître le résultat de la méthode removeFromParent? Ma première réaction a été simplement de changer le removeFromParent pour retourner un booléen indiquant si le parent a été supprimé ou non.

Mais ensuite, je suis tombé sur le modèle de séparation des requêtes de commande qui dit de ne pas faire cela.

Il indique que chaque méthode doit être soit une commande qui exécute une action, soit une requête qui renvoie des données à l'appelant, mais pas les deux. En d'autres termes, poser une question ne devrait pas changer la réponse. Plus formellement, les méthodes ne doivent renvoyer une valeur que si elles sont référentiellement transparentes et ne possèdent donc aucun effet secondaire.

Ces deux-là sont-ils vraiment en contradiction l'un avec l'autre et comment choisir entre les deux? Dois-je aller avec le programmeur pragmatique ou Bertrand Meyer à ce sujet?

Dakotah North
la source
1
que feriez-vous avec le booléen?
David
1
On dirait que vous plongez trop dans les schémas de codage sans équilibrer la convivialité
Izkata
Re booléen ... ceci est un exemple facile à utiliser mais similaire à l'opération d'écriture ci-dessous, le but est que ce soit un état de l'opération.
Dakotah North
2
mon point est que vous ne devriez pas vous concentrer sur le "retour de quelque chose" mais plutôt sur ce que vous voulez en faire. Comme je l'ai dit dans un autre commentaire, si vous vous concentrez sur l'échec, puis utilisez l'exception, si vous voulez faire quelque chose lorsque l'opération est terminée, puis utilisez le rappel ou les événements, si vous voulez enregistrer ce qui s'est passé, c'est la responsabilité du Widget ... C'est pourquoi je demande vos besoins. Si vous ne trouvez pas d'exemple concret où vous devez retourner quelque chose, cela signifie peut-être que vous n'aurez pas à choisir entre les deux.
David
1
La séparation des requêtes de commande est un principe, pas un modèle. Les modèles sont quelque chose que vous pouvez utiliser pour résoudre un problème. Les principes sont ce que vous suivez pour ne pas créer de problèmes.
candied_orange

Réponses:

25

En fait, votre exemple de problème illustre déjà le manque de décomposition.

Modifions-le un peu:

Book b = ...;
if (b.getShelf() != null) 
    b.getShelf().remove(b);

Ce n'est pas vraiment différent, mais rend la faille plus apparente: pourquoi le livre connaîtrait-il son étagère? Autrement dit, cela ne devrait pas. Il introduit une dépendance des livres sur les étagères (ce qui n'a pas de sens) et crée des références circulaires. C'est tout mauvais.

De même, il n'est pas nécessaire qu'un widget connaisse son parent. Vous direz: "D'accord, mais le widget a besoin de son parent pour se mettre en page correctement, etc." et sous le capot, le widget connaît son parent et lui demande ses métriques pour calculer ses propres métriques en fonction d'eux, etc. Selon tell, ne demandez pas que c'est faux. Le parent doit dire à tous ses enfants de rendre, en passant toutes les informations nécessaires comme arguments. De cette façon, on peut facilement avoir le même widget chez deux parents (que cela ait du sens ou non).

Pour revenir à l'exemple du livre - entrez le bibliothécaire:

Librarian l = ...;
Book b = ...;
l.findShelf(b).remove(b);

S'il vous plaît, comprenez que nous ne demandons plus dans le sens de dire, ne demandez pas .

Dans la première version, l'étagère était une propriété du livre et vous l'avez demandé. Dans la deuxième version, nous ne faisons aucune hypothèse de ce type sur le livre. Tout ce que nous savons, c'est que le bibliothécaire peut nous indiquer une étagère contenant le livre. Vraisemblablement, il s'appuie sur une sorte de table de consultation pour le faire, mais nous ne savons pas avec certitude (il pourrait également parcourir tous les livres de chaque étagère) et en fait, nous ne voulons pas savoir .
Nous ne nous appuyons pas sur la question de savoir si ou comment la réponse des bibliothécaires est couplée à son état ou à celui d'autres objets dont elle pourrait dépendre. Nous demandons au bibliothécaire de trouver l'étagère.
Dans le premier scénario, nous avons codé directement la relation entre un livre et une étagère dans le cadre de l'état du livre et récupéré cet état directement. Ceci est très fragile, car nous supposons également que l'étagère rendue par le livre contient le livre (c'est une contrainte que nous devons garantir, sinon nous pourrions ne pas être en mesure de retirer removele livre de l'étagère dans laquelle il se trouve réellement).

Avec l'introduction du bibliothécaire, nous modélisons cette relation séparément, réalisant ainsi une séparation des préoccupations.

back2dos
la source
Je pense que c'est un point très valable, mais ne répond pas du tout à la question. Dans votre version, la question est la suivante: la méthode remove (Book b) de la classe Shelf doit-elle avoir une valeur de retour?
scarfridge
1
@scarfridge: En fin de compte, dites, ne demandez pas est un corollaire d'une bonne séparation des préoccupations. Si vous y réfléchissez, je réponds donc à la question. Au moins, je pense que oui;)
back2dos
1
@scarfridge Au lieu d'une valeur de retour, on pourrait passer une fonction / délégué qui est appelé en cas d'échec. Si c'est réussi, vous avez terminé, non?
Bless Yahu
2
@BlessYahu Cela me semble trop conçu. Personnellement, je pense que la séparation commande-requête est plus un idéal dans le monde réel (multi-thread). À mon humble avis, il est normal qu'une méthode avec effets secondaires renvoie une valeur tant que le nom de la méthode indique clairement qu'elle changera l'état de l'objet. Considérez la méthode pop () d'une pile. Mais une méthode avec un nom de requête ne devrait pas avoir d'effets secondaires.
scarfridge
13

Si vous avez besoin de connaître le résultat, vous le faites; c'est votre exigence.

L'expression «les méthodes ne doivent renvoyer une valeur que si elles sont référentiellement transparentes et ne présentent donc aucun effet secondaire» est une bonne ligne directrice à suivre (surtout si vous écrivez votre programme dans un style fonctionnel , pour des accès concurrents ou pour d'autres raisons), mais c'est pas un absolu.

Par exemple, vous devrez peut-être connaître le résultat d'une opération d'écriture de fichier (vrai ou faux). C'est un exemple de méthode qui renvoie une valeur, mais produit toujours des effets secondaires; il n'y a aucun moyen de contourner cela.

Pour effectuer la séparation commande / requête, vous devez effectuer l'opération de fichier avec une méthode, puis vérifier son résultat avec une autre méthode, une technique indésirable car elle dissocie le résultat de la méthode qui a provoqué le résultat. Que se passe-t-il si quelque chose arrive à l'état de votre objet entre l'appel de fichier et la vérification d'état?

L'essentiel est le suivant: si vous utilisez le résultat d'un appel de méthode pour prendre des décisions ailleurs dans le programme, vous ne violez pas Tell Don't Ask. Si, en revanche, vous prenez des décisions pour un objet en fonction d'un appel de méthode à cet objet, vous devez déplacer ces décisions dans l'objet lui-même pour préserver l'encapsulation.

Ce n'est pas du tout contradictoire avec la séparation des requêtes de commande; en fait, il l'applique davantage, car il n'est plus nécessaire d'exposer une méthode externe à des fins de statut.

Robert Harvey
la source
1
On pourrait faire valoir que dans votre exemple, si l'opération "d'écriture" échoue, une exception doit être levée, il n'y a donc pas besoin d'une méthode "get status".
David
@David: Retombez ensuite sur l'exemple de l'OP, qui retournerait vrai si un changement se produisait réellement, faux s'il ne le faisait pas. Je doute que vous souhaitiez lancer une exception ici.
Robert Harvey
Même l'exemple du PO ne me convient pas. Soit vous voulez être remarqué si la suppression n'était pas possible, auquel cas vous utiliseriez une exception, soit vous voulez être remarqué lorsqu'un widget est réellement supprimé, auquel cas vous pouvez utiliser un mécanisme d'événement ou de rappel. Je ne vois tout simplement pas d'exemple réel où vous ne voudriez pas respecter le "modèle de séparation des requêtes de commande"
David
@David: J'ai modifié ma réponse pour clarifier.
Robert Harvey
Pour ne pas mettre un point trop fin là-dessus, mais l'idée derrière la séparation commande-requête est que quiconque émet la commande pour écrire le fichier ne se soucie pas du résultat - du moins, pas dans un sens spécifique (un utilisateur pourrait plus tard afficher une liste de toutes les opérations de fichiers récentes, mais qui n'a aucune corrélation avec la commande initiale). Si vous devez prendre des mesures après l'exécution d'une commande, que vous vous souciez ou non du résultat, la bonne façon de le faire est d'utiliser un modèle de programmation asynchrone à l'aide d' événements qui (peuvent) contenir des détails sur la commande d'origine et / ou son résultat.
Aaronaught
2

Je pense que vous devriez suivre votre instinct initial. Parfois, la conception doit être intentionnellement dangereuse, pour introduire de la complexité immédiatement lorsque vous communiquez avec l'objet afin que votre forcé le gère correctement immédiatement, au lieu d'essayer de le manipuler sur l'objet lui-même et de masquer tous les détails sanglants et de le rendre difficile à nettoyer proprement modifier les hypothèses sous-jacentes.

Vous devriez avoir un sentiment de naufrage si un objet vous propose des équivalents openet des closecomportements implémentés et vous commencez immédiatement à comprendre ce que vous traitez lorsque vous voyez une valeur de retour booléenne pour quelque chose que vous pensiez être une simple tâche atomique.

Comment vous vous en sortirez plus tard, c'est votre truc. Vous pouvez créer une abstraction au-dessus, un type de widget avec removeFromParent(), mais vous devriez toujours avoir un repli de bas niveau, sinon vous feriez peut-être des hypothèses prématurées.

N'essayez pas de tout simplifier. Il n'y a rien de plus décevant lorsque vous comptez sur quelque chose qui semble élégant et si innocent, pour réaliser que c'est la véritable horreur plus tard dans les pires moments.

Filip Dupanović
la source
2

Ce que vous décrivez est une "exception" connue au principe de séparation commande-requête.

Dans cet article, Martin Fowler explique comment il menacerait une telle exception.

Meyer aime absolument utiliser la séparation commande-requête, mais il y a des exceptions. Popping une pile est un bon exemple d'une requête qui modifie l'état. Meyer dit à juste titre que vous pouvez éviter d'avoir cette méthode, mais c'est un idiome utile. Je préfère donc suivre ce principe quand je le peux, mais je suis prêt à le casser pour obtenir ma pop.

Dans votre exemple, je considérerais la même exception.

elviejo79
la source
0

La séparation commande-requête est incroyablement facile à comprendre.

Si je vous dis dans mon système il y a une commande qui retourne également une valeur à partir d'une requête et vous dites "Ha! Vous êtes en violation!" vous sautez le pistolet.

Non, ce n'est pas ce qui est interdit.

Ce qui est interdit, c'est quand cette commande est le SEUL moyen de faire cette requête. Non. Je ne devrais pas avoir à changer d'état pour poser des questions. Cela ne signifie pas que je dois fermer les yeux chaque fois que je change d'état.

Peu importe si je le fais car je vais juste les rouvrir de toute façon. Le seul avantage de cette réaction excessive était de garantir que les concepteurs ne soient pas paresseux et oublient d'inclure la requête sans changement d'état.

Le vrai sens est si sacrément subtil qu'il est juste plus facile pour les paresseux de dire que les colons devraient revenir sans effet. Cela permet de s'assurer plus facilement que vous ne violez pas la vraie règle, mais c'est une réaction excessive qui peut devenir une vraie douleur.

Les interfaces fluides et les iDSL "violent" cette réaction excessive tout le temps. Il y a beaucoup de pouvoir ignoré si vous réagissez trop.

Vous pouvez faire valoir qu'une commande ne doit faire qu'une seule chose et qu'une requête ne doit faire qu'une seule chose. Et dans de nombreux cas, c'est un bon point. Qui peut dire qu'il n'y a qu'une seule requête qui devrait suivre une commande? Très bien, mais ce n'est pas la séparation commande-requête. C'est le principe de la responsabilité unique.

Vu sous cet angle, un stack pop n'est pas une exception bizarre. C'est une seule responsabilité. Pop ne viole la séparation des requêtes de commande que si vous oubliez de fournir un aperçu.

candied_orange
la source