Wie kann man zwischen Tell Don't Ask und Command Query Separation wählen?

25

Das Prinzip Tell Don't Ask besagt:

Sie sollten versuchen, Objekten mitzuteilen, was sie tun sollen. Fragen Sie sie nicht nach ihrem Zustand, treffen Sie keine Entscheidung und sagen Sie ihnen dann, was sie tun sollen.

Das Problem ist, dass Sie als Aufrufer keine Entscheidungen treffen sollten, die auf dem Status des aufgerufenen Objekts basieren und zu einer Änderung des Status des Objekts führen. Die Logik, die Sie implementieren, liegt wahrscheinlich in der Verantwortung des aufgerufenen Objekts, nicht in Ihrer. Wenn Sie Entscheidungen außerhalb des Objekts treffen, wird dessen Kapselung verletzt.

Ein einfaches Beispiel für "Tell, Don't Ask" ist

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

und die tell version ist ...

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

Was ist, wenn ich das Ergebnis der removeFromParent-Methode kennen muss? Meine erste Reaktion bestand lediglich darin, removeFromParent so zu ändern, dass ein Boolescher Wert zurückgegeben wird, der angibt, ob das übergeordnete Element entfernt wurde oder nicht.

Aber dann bin ich auf das Trennmuster für Befehlsabfragen gestoßen, das besagt, dass dies NICHT erfolgen soll.

Jede Methode sollte entweder ein Befehl sein, der eine Aktion ausführt, oder eine Abfrage, die Daten an den Aufrufer zurückgibt, jedoch nicht beide. Mit anderen Worten, das Stellen einer Frage sollte die Antwort nicht ändern. Formal sollten Methoden nur dann einen Wert zurückgeben, wenn sie referenziell transparent sind und daher keine Nebenwirkungen aufweisen.

Sind diese beiden wirklich uneins und wie wähle ich zwischen den beiden? Gehe ich mit dem Pragmatic Programmer oder Bertrand Meyer darüber?

Dakotah North
quelle
1
was würdest du mit dem Booleschen machen?
David
1
Es hört sich so an, als würden Sie zu weit in Codierungsmuster
eintauchen
Re boolean ... dies ist ein Beispiel, das sich leicht zurückschieben ließ, aber ähnlich wie bei der unten stehenden Schreiboperation ist das Ziel, dass es ein Status der Operation ist.
Dakotah North
2
Mein Punkt ist, dass Sie sich nicht darauf konzentrieren sollten, "etwas zurückzugeben", sondern mehr darauf, was Sie damit tun möchten. Wie ich in einem anderen Kommentar sagte, wenn Sie sich auf Fehler konzentrieren, dann verwenden Sie Ausnahme, wenn Sie etwas tun möchten, wenn der Vorgang abgeschlossen ist, dann verwenden Sie Rückruf oder Ereignisse, wenn Sie protokollieren möchten, was passiert ist, liegt es in der Verantwortung des Widgets ... Das ist warum frage ich nach deinen Bedürfnissen. Wenn Sie kein konkretes Beispiel finden, bei dem Sie etwas zurückgeben müssen, bedeutet dies möglicherweise, dass Sie sich nicht zwischen beiden entscheiden müssen.
David
1
Die Befehlsabfragetrennung ist ein Prinzip, kein Muster. Mit Mustern können Sie ein Problem lösen. Prinzipien sind das, was Sie befolgen, um keine Probleme zu verursachen.
candied_orange

Antworten:

25

Tatsächlich zeigt Ihr Beispielproblem bereits einen Mangel an Zersetzung.

Lass es uns einfach ein bisschen ändern:

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

Das ist nicht wirklich anders, macht aber den Fehler deutlicher: Warum sollte das Buch über sein Regal Bescheid wissen? Einfach gesagt, sollte es nicht. Es wird eine Abhängigkeit von Büchern in Regalen eingeführt (was keinen Sinn ergibt) und Zirkelverweise erstellt. Es ist alles schlecht.

Ebenso ist es nicht erforderlich, dass ein Widget sein übergeordnetes Element kennt. Sie werden sagen: "Ok, aber das Widget benötigt seine Eltern, um sich selbst richtig zu gestalten usw." und unter der Haube des Widget kennt seine Eltern und fragt sie für ihre Metriken seine eigenen Metriken zu berechnen , basierend auf sie usw. Nach sagen, nicht fragen , das ist falsch. Das übergeordnete Element sollte allen untergeordneten Elementen das Rendern anweisen und alle erforderlichen Informationen als Argumente übergeben. Auf diese Weise kann man leicht dasselbe Widget in zwei Elternteilen haben (unabhängig davon, ob dies tatsächlich Sinn macht oder nicht).

Um zum Buchbeispiel zurückzukehren, geben Sie den Bibliothekar ein:

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

Bitte haben Sie Verständnis, dass wir nicht fragen , mehr im Sinne von tell, fragen Sie nicht .

In der ersten Version war das Regal Eigentum des Buches und Sie haben danach gefragt. In der zweiten Version machen wir keine solche Annahme über das Buch. Wir wissen nur, dass der Bibliothekar uns ein Regal mit dem Buch mitteilen kann. Vermutlich verlässt er sich dazu auf eine Art Nachschlagetabelle, aber wir wissen es nicht genau (er könnte auch einfach alle Bücher in jedem Regal durchgehen) und eigentlich wollen wir es nicht wissen .
Wir verlassen uns nicht darauf, ob oder wie die Reaktion der Bibliothekare an ihren Zustand oder den anderer Objekte gekoppelt ist, von denen sie abhängig sein könnten. Wir bitten den Bibliothekar, das Regal zu finden.
Im ersten Szenario haben wir die Beziehung zwischen einem Buch und einem Regal als Teil des Buchstatus direkt codiert und diesen Status direkt abgerufen. Dies ist sehr fragil, da wir auch davon ausgehen, dass das vom Buch zurückgegebene Regal das Buch enthält (dies ist eine Einschränkung, die wir sicherstellen müssen, da wir sonst möglicherweise nicht in removeder Lage sind, das Buch aus dem Regal zu entnehmen, in dem es sich tatsächlich befindet).

Mit der Einführung des Bibliothekars modellieren wir diese Beziehung separat und erreichen so eine Trennung der Anliegen.

back2dos
quelle
Ich denke, das ist ein sehr zutreffender Punkt, aber beantwortet die Frage überhaupt nicht. In Ihrer Version stellt sich die Frage, ob die Methode remove (Book b) in der Shelf-Klasse einen Rückgabewert hat.
scarfridge
1
@scarfridge: Unter dem Strich, sag, frag nicht, ist eine Folge der richtigen Trennung von Bedenken. Wenn Sie darüber nachdenken, beantworte ich die Frage. Zumindest würde ich so denken;)
back2dos
1
@scarfridge Anstelle eines Rückgabewerts könnte man eine Funktion / einen Delegaten übergeben, die / der bei einem Fehler aufgerufen wird. Wenn es gelingt, sind Sie fertig, oder?
Segne Yahu
2
@BlessYahu Das scheint mir überentwickelt. Ich persönlich bin der Meinung, dass die Trennung von Befehlen und Abfragen in der realen Welt (mit mehreren Threads) eher ein Ideal ist. IMHO ist es in Ordnung, wenn eine Methode mit Nebenwirkungen einen Wert zurückgibt, solange der Name der Methode eindeutig angibt, dass sich der Status des Objekts ändert. Betrachten Sie die pop () -Methode eines Stapels. Eine Methode mit einem Abfragenamen sollte jedoch keine Nebenwirkungen haben.
scarfridge
13

Wenn Sie das Ergebnis wissen müssen, dann tun Sie es; Das ist Ihre Anforderung.

Der Ausdruck "Methoden sollten nur dann einen Wert zurückgeben, wenn sie referenziell transparent sind und daher keine Nebenwirkungen aufweisen" ist eine gute Richtlinie (insbesondere, wenn Sie Ihr Programm in einem funktionalen Stil schreiben , aus Parallelitätsgründen oder aus anderen Gründen) kein absolutes.

Möglicherweise müssen Sie das Ergebnis eines Dateischreibvorgangs kennen (true oder false). Dies ist ein Beispiel für eine Methode, die einen Wert zurückgibt, jedoch immer Nebenwirkungen hervorruft. Daran führt kein Weg vorbei.

Um die Befehls- / Abfragetrennung zu erfüllen, müssen Sie die Dateioperation mit einer Methode ausführen und anschließend das Ergebnis mit einer anderen Methode überprüfen. Dies ist eine unerwünschte Technik, da das Ergebnis von der Methode entkoppelt wird, die das Ergebnis verursacht hat. Was passiert, wenn zwischen dem Dateiaufruf und der Statusprüfung etwas mit dem Status Ihres Objekts passiert?

Die Quintessenz lautet: Wenn Sie das Ergebnis eines Methodenaufrufs verwenden , um an anderer Stelle im Programm Entscheidungen zu treffen, verletzen Sie Tell Don't Ask nicht. Wenn Sie andererseits Entscheidungen für ein Objekt auf der Grundlage eines Methodenaufrufs für dieses Objekt treffen, sollten Sie diese Entscheidungen in das Objekt selbst verschieben , um die Kapselung beizubehalten.

Dies steht in keinerlei Widerspruch zur Befehlsabfragetrennung. Tatsächlich wird es weiter erzwungen, da keine externe Methode mehr für Statuszwecke verfügbar gemacht werden muss.

Robert Harvey
quelle
1
Man könnte argumentieren, dass in Ihrem Beispiel, wenn die "Schreib" -Operation fehlschlägt, eine Ausnahme ausgelöst werden sollte, sodass keine "Status abrufen" -Methode erforderlich ist.
David
@David: Dann greifen Sie auf das Beispiel des OP zurück, das true zurückgibt, wenn eine Änderung tatsächlich auftritt, und false, wenn dies nicht der Fall ist. Ich bezweifle, dass Sie dort eine Ausnahme machen wollen.
Robert Harvey
Selbst das Beispiel des OP fühlt sich für mich nicht richtig an. Entweder möchten Sie bemerkt werden, wenn das Entfernen nicht möglich war. In diesem Fall würden Sie eine Ausnahme verwenden, oder Sie möchten bemerkt werden, wenn ein Widget tatsächlich entfernt wird. In diesem Fall könnten Sie einen Ereignis- oder Rückrufmechanismus verwenden. Ich sehe nur kein richtiges Beispiel, in dem Sie das "Command Query Separation Pattern" nicht beachten möchten
David
@ David: Ich habe meine Antwort bearbeitet, um zu klären.
Robert Harvey
Um es nicht zu genau zu sagen, aber die ganze Idee hinter der Trennung von Befehlen und Abfragen ist, dass es demjenigen, der den Befehl zum Schreiben der Datei ausgibt, egal ist, was passiert - zumindest nicht in einem bestimmten Sinne (ein Benutzer könnte es später tun) eine Liste aller zuletzt ausgeführten Dateivorgänge abrufen, die jedoch nicht mit dem ursprünglichen Befehl korrelieren). Wenn Sie nach Abschluss eines Befehls Maßnahmen ergreifen müssen, unabhängig davon, ob Sie sich für das Ergebnis interessieren oder nicht, können Sie dies bei einem asynchronen Programmiermodell mit Ereignissen tun, die (möglicherweise) Details zum ursprünglichen Befehl und / oder zu dessen Ergebnissen enthalten Ergebnis.
Aaronaught
2

Ich denke, dass Sie mit Ihrem anfänglichen Instinkt gehen sollten. Manchmal sollte das Design absichtlich gefährlich sein und sofort Komplexität erzeugen, wenn Sie mit dem Objekt kommunizieren, sodass Sie es sofort richtig handhaben müssen, anstatt zu versuchen, es am Objekt selbst zu handhaben, alle blutigen Details zu verbergen und es schwierig zu machen, es sauber zu halten die zugrunde liegenden Annahmen ändern.

Sie sollten ein sinkendes Gefühl bekommen, wenn ein Objekt Ihnen implementierte Äquivalente openund closeVerhaltensweisen bietet und Sie sofort beginnen zu verstehen, womit Sie es zu tun haben, wenn Sie einen booleschen Rückgabewert für etwas sehen, von dem Sie dachten, dass es eine einfache atomare Aufgabe wäre.

Wie Sie später damit umgehen, ist Ihre Sache. Sie können darüber eine Abstraktion erstellen, einen Widget-Typ mit removeFromParent(), aber Sie sollten immer einen Low-Level-Fallback haben, da Sie sonst möglicherweise verfrühte Annahmen getroffen haben.

Versuche nicht alles einfach zu machen. Es gibt nichts Enttäuschenderes, als sich auf etwas zu verlassen, das elegant und unschuldig erscheint, um später im schlimmsten Moment zu erkennen, dass es wahrer Horror ist.

Filip Dupanović
quelle
2

Was Sie beschreiben, ist eine bekannte "Ausnahme" des Befehlsabfragetrennungsprinzips.

In diesem Artikel erklärt Martin Fowler , wie er eine solche Ausnahme bedrohen würde.

Meyer verwendet die Trennung von Befehlen und Abfragen gern, aber es gibt Ausnahmen. Das Poppen eines Stapels ist ein gutes Beispiel für eine Abfrage, die den Status ändert. Meyer sagt zu Recht, dass Sie diese Methode vermeiden können, aber es ist eine nützliche Redewendung. Also folge ich lieber diesem Prinzip, wenn ich kann, aber ich bin bereit, es zu brechen, um meinen Pop zu bekommen.

In Ihrem Beispiel würde ich die gleiche Ausnahme betrachten.

elviejo79
quelle
0

Die Trennung von Befehlen und Abfragen ist erstaunlich leicht zu missverstehen.

Wenn ich Ihnen in meinem System sage, dass es einen Befehl gibt, der auch einen Wert aus einer Abfrage zurückgibt, und Sie sagen: "Ha! Sie sind verletzt!" Du springst die Waffe.

Nein, das ist nicht verboten.

Was verboten ist, ist, wenn dieser Befehl die EINZIGE Möglichkeit ist, diese Abfrage durchzuführen. Nein, ich sollte nicht den Status ändern müssen, um Fragen zu stellen. Das bedeutet nicht, dass ich meine Augen jedes Mal schließen muss, wenn ich den Zustand ändere.

Ist mir egal, da ich sie sowieso einfach wieder öffne. Der einzige Vorteil dieser Überreaktion war, dass die Designer nicht faul wurden und die Abfrage, die den Status nicht ändert, nicht vergessen.

Die wahre Bedeutung ist so subtil, dass es für faule Leute einfacher ist, zu sagen, Setter sollten für ungültig erklären. Dies macht es einfacher, sicherzugehen, dass Sie nicht gegen die wirkliche Regel verstoßen, aber es ist eine Überreaktion, die zu einem echten Schmerz werden kann.

Fließende Schnittstellen und iDSLs "verletzen" diese Überreaktion die ganze Zeit. Es wird viel Kraft ignoriert, wenn Sie überreagieren.

Sie können argumentieren, dass ein Befehl nur eine Sache tun sollte und eine Abfrage nur eine Sache tun sollte. Und in vielen Fällen ist das ein guter Punkt. Wer sagt, dass es immer nur eine Abfrage gibt, die einem Befehl folgen sollte? Gut, aber das ist keine Trennung von Befehlen und Abfragen. Das ist das Prinzip der Einzelverantwortung.

So gesehen ist ein Stack-Pop keine bizarre Ausnahme. Es ist eine einzelne Verantwortung. Pop verletzt die Trennung von Befehlsabfragen nur, wenn Sie vergessen, peek anzugeben.

kandierte_orange
quelle