Erklärung, wie "Tell, Don't Ask" als guter OO angesehen wird

49

Dieser Blogpost wurde in den Hacker News mit mehreren positiven Stimmen gepostet. Aus C ++ stammend, scheinen die meisten dieser Beispiele gegen das zu verstoßen, was mir beigebracht wurde.

Zum Beispiel # 2:

Schlecht:

def check_for_overheating(system_monitor)
  if system_monitor.temperature > 100
    system_monitor.sound_alarms
  end
end

gegen gut:

system_monitor.check_for_overheating

class SystemMonitor
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

Der Rat in C ++ ist, dass Sie freie Funktionen anstelle von Member-Funktionen bevorzugen sollten, da diese die Kapselung erhöhen. Beide sind semantisch identisch. Warum also die Wahl bevorzugen, die Zugriff auf mehr Status hat?

Beispiel 4:

Schlecht:

def street_name(user)
  if user.address
    user.address.street_name
  else
    'No street name on file'
  end
end

gegen gut:

def street_name(user)
  user.address.street_name
end

class User
  def address
    @address || NullAddress.new
  end
end

class NullAddress
  def street_name
    'No street name on file'
  end
end

Warum liegt es in der Verantwortung User, eine nicht verwandte Fehlerzeichenfolge zu formatieren? Was ist, wenn ich etwas anderes als Drucken machen möchte, 'No street name on file'wenn es keine Straße gibt? Was ist, wenn die Straße den gleichen Namen hat?


Könnte mich jemand über die Vorteile und Gründe von "Tell, Don't Ask" aufklären? Ich suche nicht, was besser ist, sondern versuche, den Standpunkt des Autors zu verstehen.

Pubby
quelle
Codebeispiele könnten Ruby und nicht Python sein, keine Ahnung.
Pubby
2
Ich frage mich immer, ob so etwas wie das erste Beispiel nicht eher eine Verletzung von SRP ist.
Stijn
1
Sie können dies lesen: pragprog.com/articles/tell-dont-ask
Mik378
Rubin. @ ist zum Beispiel eine Kurzform und Python beendet seine Blöcke implizit mit Leerzeichen.
Erik Reppen
3
"Der Rat in C ++ ist, dass Sie freie Funktionen anstelle von Member-Funktionen bevorzugen sollten, da diese die Kapselung erhöhen." Ich weiß nicht, wer dir das gesagt hat, aber es ist nicht wahr. Freie Funktionen können verwendet werden, um die Einkapselung zu erhöhen, aber sie erhöhen nicht unbedingt die Einkapselung.
Rob K

Antworten:

81

Wenn Sie das Objekt nach seinem Status fragen und dann Methoden für dieses Objekt aufrufen, die auf Entscheidungen außerhalb des Objekts basieren, bedeutet dies, dass das Objekt jetzt eine undichte Abstraktion ist. Ein Teil seines Verhaltens befindet sich außerhalb des Objekts, und der interne Zustand ist (möglicherweise unnötigerweise) der Außenwelt ausgesetzt.

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.

Klar, kann man sagen, das liegt auf der Hand. Ich würde niemals solchen Code schreiben. Trotzdem ist es sehr einfach, sich in das Untersuchen eines referenzierten Objekts und das Aufrufen verschiedener Methoden auf der Grundlage der Ergebnisse einzulullen. Aber das ist vielleicht nicht der beste Weg, dies zu tun. Sagen Sie dem Objekt, was Sie wollen. Lassen Sie es herausfinden, wie es geht. Denken Sie deklarativ statt prozedural!

Es ist einfacher, sich aus dieser Falle herauszuhalten, wenn Sie zunächst Klassen auf der Grundlage ihrer Verantwortlichkeiten entwerfen. Sie können dann auf natürliche Weise Befehle angeben, die von der Klasse ausgeführt werden dürfen, und nicht Abfragen, die Sie über den Status des Objekts informieren.

http://pragprog.com/articles/tell-dont-ask

Robert Harvey
quelle
4
Der Beispieltext verbietet viele Dinge, die eindeutig eine gute Praxis sind.
DeadMG
13
@DeadMG es tut , was Sie sagen , nur diejenigen es sklavisch folgen, die „pragmatische“ im Site - Namen und Schlüssel Gedanken an blind ignorieren Website Autoren , die in ihren Schlüssel Buch eindeutig erklärt wurde: „Es gibt nicht so etwas wie eine beste Lösung ... "
gnat
2
Lies niemals das Buch. Ich würde es auch nicht wollen. Ich habe nur den Beispieltext gelesen, der völlig fair ist.
DeadMG
3
@DeadMG keine Sorgen. Nun, da Sie den entscheidenden Punkt kennen, der dieses Beispiel (und jedes andere von pragprog) in den beabsichtigten Kontext stellt ("keine beste Lösung ..."), ist es in Ordnung, das Buch nicht zu lesen
gnat
1
Ich bin mir immer noch nicht sicher, was Tell, Don't Ask für Sie ohne Kontext bedeuten soll, aber dies ist wirklich ein guter OOP-Rat.
Erik Reppen
16

Im Allgemeinen schlägt das Stück vor, dass Sie den Mitgliedsstaat nicht anderen zur Diskussion stellen sollten, wenn Sie selbst darüber urteilen könnten .

Unklar ist jedoch, dass dieses Gesetz in ganz offensichtliche Grenzen stößt, wenn die Argumentation weit über der Verantwortung einer bestimmten Klasse liegt. Zum Beispiel jede Klasse, deren Aufgabe es ist, einen Wert zu halten oder einen Wert bereitzustellen - insbesondere einen generischen - oder bei der die Klasse ein Verhalten bereitstellt, das erweitert werden muss.

Wenn beispielsweise das System das temperatureals Abfrage bereitstellt , dann kann der Client morgen check_for_underheatingohne Änderung SystemMonitor. Dies ist nicht der Fall, wenn das SystemMonitorGerät check_for_overheatingselbst arbeitet. SystemMonitorDem folgt also eine Klasse, deren Aufgabe es ist, einen Alarm auszulösen, wenn SystemMonitordie Temperatur zu hoch ist - aber eine Klasse, deren Aufgabe es ist, einem anderen Teil des Codes das Lesen der Temperatur zu ermöglichen, damit er beispielsweise TurboBoost oder ähnliches steuern kann , sollte nicht.

Beachten Sie auch, dass das zweite Beispiel sinnlos das Null-Objekt-Anti-Muster verwendet.

DeadMG
quelle
19
"Null-Objekt" ist nicht das, was ich als Anti-Muster bezeichnen würde, also frage ich mich, warum Sie das tun?
Konrad Rudolph
4
Ziemlich sicher, dass niemand Methoden hat, die als "Tut nichts" angegeben sind. Das macht es sinnlos, sie anzurufen. Das bedeutet, dass jedes Objekt, das Null Object implementiert, zumindest LSP bricht und sich selbst als Implementierungsoperationen beschreibt, die es tatsächlich nicht ausführt. Der Benutzer erwartet einen Wert zurück. Die Richtigkeit ihres Programms hängt davon ab. Sie bringen einfach mehr Probleme mit sich, indem Sie so tun, als wäre es ein Wert, wenn dies nicht der Fall ist. Haben Sie jemals versucht, im Hintergrund fehlgeschlagene Methoden zu debuggen? Es ist unmöglich und niemand sollte jemals darunter leiden müssen.
DeadMG
4
Ich würde argumentieren, dass dies völlig von der Problemdomäne abhängt.
Konrad Rudolph
5
@DeadMG Ich bin damit einverstanden, dass das obige Beispiel eine schlechte Verwendung des Null-Objektmusters ist, aber es hat einen Vorteil, es zu verwenden. Einige Male habe ich eine "No-Op" -Implementierung einer Schnittstelle oder einer anderen verwendet, um zu vermeiden, dass das System auf Null überprüft wird oder ein echtes "Null" vorliegt.
Max
6
Ich bin mir nicht sicher, ob ich deinen Standpunkt mit "der Client kann, check_for_underheatingohne sich ändern zu müssen SystemMonitor" verstehe . Wie unterscheidet sich der Kunde SystemMonitorzu diesem Zeitpunkt? Verteilen Sie Ihre Überwachungslogik jetzt nicht auf mehrere Klassen? Ich sehe auch kein Problem mit einer Monitorklasse, die andere Klassen mit Sensorinformationen versorgt und gleichzeitig die Alarmfunktionen für sich selbst reserviert. Der Boost-Regler sollte den Boost regeln, ohne sich darum kümmern zu müssen, einen Alarm auszulösen, wenn die Temperatur zu hoch wird.
TMN
9

Das eigentliche Problem bei Ihrem Überhitzungsbeispiel besteht darin, dass die Regeln für das, was als Überhitzung bezeichnet wird, für verschiedene Systeme nicht leicht geändert werden können. Angenommen, System A ist so, wie Sie es haben (Temp.> 100 ist überhitzt), aber System B ist empfindlicher (Temp.> 93 ist überhitzt). Ändern Sie Ihre Steuerfunktion, um den Systemtyp zu überprüfen und dann den richtigen Wert anzuwenden?

if (system is a System_A and system_monitor.temp >100)
  system_monitor.sound_alarms
else if (system is a System_B and system_monitor.temp > 93)
  system_monitor.sound_alarms
end

Oder hat jeder Anlagentyp seine Heizleistung definiert?

BEARBEITEN:

system.check_for_overheating

class SystemA : System
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

class SystemB : System
  def check_for_overheating
    if temperature > 93
      sound_alarms
    end
  end
end

Bei der ersten Methode wird Ihre Steuerungsfunktion hässlich, wenn Sie anfangen, sich mit mehr Systemen zu befassen. Letzteres sorgt dafür, dass die Steuerfunktion im Laufe der Zeit stabil bleibt.

Matthew Flynn
quelle
1
Warum nicht jedes System beim Monitor registrieren lassen? Während der Registrierung können sie anzeigen, wenn eine Überhitzung auftritt.
Martin York
@LokiAstari - Sie könnten, aber dann könnten Sie auf ein neues System stoßen, das auch empfindlich auf Feuchtigkeit oder atmosphärischen Druck reagiert. Das Prinzip ist herauszufinden, was variiert - in diesem Fall ist es die Anfälligkeit für Überhitzung
Matthew Flynn
1
Genau aus diesem Grund sollten Sie ein Tell-Modell haben. Sie teilen dem System die aktuellen Bedingungen mit und es informiert Sie, wenn es sich außerhalb der normalen Arbeitsbedingungen befindet. Auf diese Weise müssen Sie den SystemMoniter niemals ändern. Das ist eine Kapselung für Sie.
Martin York
@LokiAstari - Ich denke, wir reden hier über verschiedene Zwecke - Ich habe wirklich versucht, verschiedene Systeme zu erstellen, anstatt verschiedene Monitore. Die Sache ist, das System sollte wissen, wann es sich in einem Zustand befindet, der einen Alarm auslöst, im Gegensatz zu einigen Funktionen eines externen Controllers. SystemA sollte seine Kriterien haben, SystemB sollte seine eigenen haben. Die Steuerung sollte nur in der Lage sein (in regelmäßigen Abständen) zu fragen, ob das System in Ordnung ist oder nicht.
Matthew Flynn
6

Zunächst einmal bin ich der Meinung, dass ich Ihrer Charakterisierung der Beispiele als "schlecht" und "gut" eine Ausnahme machen muss. In dem Artikel werden die Begriffe "Nicht so gut" und "Besser" verwendet. Ich denke, diese Begriffe wurden aus einem Grund ausgewählt: Dies sind Richtlinien, und je nach den Umständen kann der Ansatz "Nicht so gut" angemessen oder die einzige Lösung sein.

Wenn Sie die Wahl haben, sollten Sie es vorziehen , Funktionen, die ausschließlich von der Klasse abhängen, in die Klasse einzubeziehen, anstatt außerhalb der Klasse. Der Grund hierfür liegt in der Kapselung und der Tatsache, dass die Entwicklung der Klasse im Laufe der Zeit einfacher wird. Die Klasse macht auch bessere Werbung für ihre Fähigkeiten als eine Reihe von kostenlosen Funktionen.

Manchmal muss man es sagen, weil die Entscheidung von etwas außerhalb der Klasse abhängt oder weil es einfach etwas ist, das die meisten Benutzer der Klasse nicht wollen. Manchmal möchten Sie dies mitteilen, da das Verhalten für die Klasse nicht intuitiv ist und Sie die meisten Benutzer der Klasse nicht verwirren möchten.

Zum Beispiel beschweren Sie sich, dass die Straße eine Fehlermeldung zurückgibt. Dies ist nicht der Fall, sondern die Angabe eines Standardwerts. Aber manchmal ist ein Standardwert nicht angemessen. Wenn dies Bundesstaat oder Stadt war, möchten Sie möglicherweise eine Standardeinstellung, wenn Sie einem Verkäufer oder einem Umfrageteilnehmer einen Datensatz zuweisen, damit alle Unbekannten an eine bestimmte Person weitergeleitet werden. Wenn Sie dagegen Umschläge drucken, bevorzugen Sie möglicherweise eine Ausnahme oder einen Schutz, der Sie davon abhält, Papier für Briefe zu verschwenden, die nicht zugestellt werden können.

Es kann also Fälle geben, in denen "Nicht so gut" der richtige Weg ist, aber im Allgemeinen "Besser" besser ist.

jmoreno
quelle
3

Daten / Objekt-Antisymmetrie

Wie bereits erwähnt, ist Tell-Dont-Ask speziell für Fälle gedacht, in denen Sie den Objektstatus nach Ihrer Anfrage ändern (siehe z. B. den Pragprog-Text an einer anderen Stelle auf dieser Seite). Dies ist nicht immer der Fall, z. B. wird das Benutzerobjekt nicht geändert, nachdem es nach seiner Benutzeradresse gefragt wurde. Es ist daher fraglich, ob dies ein angemessener Fall ist, um Tell-Dont-Ask anzuwenden.

Tell-Dont-Ask befasst sich mit der Verantwortung, keine Logik aus einer Klasse herauszuholen, die zu Recht darin enthalten sein sollte. Aber nicht jede Logik, die sich mit Objekten befasst, ist notwendigerweise die Logik dieser Objekte. Dies deutet auf eine tiefere Ebene hin, sogar über Tell-Dont-Ask hinaus, und ich möchte eine kurze Bemerkung dazu hinzufügen.

Im Hinblick auf die architektonische Gestaltung möchten Sie möglicherweise Objekte haben, die eigentlich nur Container für Eigenschaften sind, möglicherweise sogar unveränderlich, und dann verschiedene Funktionen über Sammlungen solcher Objekte ausführen, diese auswerten, filtern oder transformieren, anstatt ihnen Befehle zu senden (d. H mehr die Domain von Tell-Dont-Ask).

Die Entscheidung, die für Ihr Problem besser geeignet ist, hängt davon ab, ob Sie stabile Daten (deklarative Objekte) erwarten, diese aber auf der Funktionsseite ändern / hinzufügen. Oder wenn Sie einen stabilen und eingeschränkten Satz solcher Funktionen erwarten, aber mehr Fluss auf Objektebene erwarten, z. B. durch Hinzufügen neuer Typen. In der ersten Situation würden Sie freie Funktionen bevorzugen, in der zweiten Objektmethode.

Bob Martin nennt dies in seinem Buch "Clean Code" "Data / Object Anti-Symmetry" (S. 95 ff.), Andere Communities könnten es als " Ausdrucksproblem " bezeichnen.

ThomasH
quelle
3

Dieses Paradigma wird manchmal als "Sagen, nicht fragen" bezeichnet , was bedeutet, dem Objekt zu sagen, was zu tun ist, und nicht nach seinem Zustand zu fragen. und manchmal als "Fragen, nicht erzählen" , was bedeutet, das Objekt zu bitten, etwas für Sie zu tun, es nicht zu sagen, wie sein Zustand sein sollte. Die beste Vorgehensweise ist in beiden Fällen gleich: Die Art und Weise, in der ein Objekt eine Aktion ausführen soll, betrifft das Objekt und nicht das aufrufende Objekt. Schnittstellen sollten vermeiden, ihren Status offenzulegen (z. B. über Accessoren oder öffentliche Eigenschaften), und stattdessen "Doing" -Methoden offenlegen, deren Implementierung undurchsichtig ist. Andere haben dies mit den Links zu pragmatischen Programmierern abgedeckt.

Diese Regel von der Regel verwendet ist über „Doppelpunkt“ oder „Doppelpfeil“ Code, die oft als "nur reden sofortige Freunde zu vermeiden, in dem es heißt foo->getBar()->doSomething()schlecht ist , verwenden Sie stattdessen foo->doSomething();die ein Wrapper Anruf um Bar Funktionalität ist, und einfach umgesetzt return bar->doSomething();- wenn fooman für das Management verantwortlich ist bar, dann lass es tun!

Nicholas Shanks
quelle
1

Zusätzlich zu den anderen guten Antworten zu "Tell, Don't Ask", einige Kommentare zu Ihren spezifischen Beispielen, die hilfreich sein könnten:

Der Rat in C ++ ist, dass Sie freie Funktionen anstelle von Member-Funktionen bevorzugen sollten, da diese die Kapselung erhöhen. Beide sind semantisch identisch. Warum also die Wahl bevorzugen, die Zugriff auf mehr Status hat?

Diese Wahl hat keinen Zugang zu mehr Staat. Beide verwenden dieselbe Menge an Staat, um ihre Arbeit zu erledigen, aber das „schlechte“ Beispiel verlangt, dass der Klassenstaat öffentlich ist, um seine Arbeit zu erledigen. Darüber hinaus wird das Verhalten dieser Klasse im "schlechten" Beispiel auf die freie Funktion ausgedehnt, was es schwieriger macht, sie zu finden und schwieriger umzugestalten.

Warum liegt es in der Verantwortung des Benutzers, eine nicht verwandte Fehlerzeichenfolge zu formatieren? Was ist, wenn ich etwas anderes tun möchte als 'Kein Straßenname in Datei' zu drucken, wenn es keine Straße gibt? Was ist, wenn die Straße den gleichen Namen hat?

Warum liegt es in der Verantwortung von 'street_name', sowohl 'get street name' als auch 'supply error message' auszuführen? Zumindest in der "guten" Version hat jedes Stück eine Verantwortung. Trotzdem ist es kein gutes Beispiel.

Telastyn
quelle
2
Das ist nicht wahr. Sie nehmen an, dass die Überprüfung auf Überhitzung die einzig vernünftige Sache ist, die mit der Temperatur zu tun hat. Was ist, wenn die Klasse einer von mehreren Temperaturüberwachungsgeräten sein soll und ein System abhängig von vielen ihrer Ergebnisse unterschiedliche Maßnahmen ergreifen muss? Wenn dieses Verhalten kann begrenzt werden , um das Verhalten einer einzelnen Instanz vordefinierte, dann sicher. Ansonsten kann es natürlich nicht zutreffen.
DeadMG
Sicher, oder ob der Thermostat und die Alarme in verschiedenen Klassen existierten (wie sie wahrscheinlich sollten).
Telastyn
1
@DeadMG: Der allgemeine Rat ist, Dinge privat / geschützt zu machen, bis Sie Zugriff auf sie benötigen. Während dieses spezielle Beispiel meh ist, bestreitet das nicht die Standardpraxis.
Guvante
Ein Beispiel in einem Artikel über die Praxis , "meh" zu sein, bestreitet dies irgendwie. Wenn diese Praxis aufgrund ihrer großen Vorteile Standard ist, warum dann die Schwierigkeit, ein geeignetes Beispiel zu finden?
Stijn de Witt
1

Diese Antworten sind sehr gut, aber hier ist ein weiteres Beispiel, um es zu betonen: Beachten Sie, dass es normalerweise eine Möglichkeit ist, Doppelarbeit zu vermeiden. Nehmen wir zum Beispiel an, Sie haben MEHRERE Stellen mit einem Code wie:

Product product = productMgr.get(productUuid)
if (product.userUuid != currentUser.uuid) {
    throw BlahException("This product doesn't belong to this user")
}

Das heißt, du solltest besser so etwas haben:

Product product = productMgr.get(productUuid, currentUser)

Da diese Duplizierung bedeutet, dass die meisten Clients Ihrer Schnittstelle die neue Methode verwenden, anstatt hier und da dieselbe Logik zu wiederholen. Sie geben Ihrem Delegierten die Arbeit, die Sie erledigen möchten, anstatt nach den Informationen zu fragen, die Sie benötigen, um sie selbst zu erledigen.

antonio.fornie
quelle
0

Ich glaube, dass dies beim Schreiben von Objekten auf hoher Ebene wahrer ist, aber weniger, wenn man in die tiefere Ebene, z. B. die Klassenbibliothek, geht, da es unmöglich ist, jede einzelne Methode zu schreiben, um alle Klassenkonsumenten zufriedenzustellen.

Zum Beispiel # 2 denke ich, dass es zu stark vereinfacht ist. Wenn wir dies tatsächlich implementieren würden, würde der SystemMonitor den Code für den Hardwarezugriff auf niedriger Ebene und die Logik für die Abstraktion auf hoher Ebene in dieselbe Klasse einbetten. Wenn wir versuchen, dies in zwei Klassen zu unterteilen, verstoßen wir leider gegen das "Tell, Don't Ask" -Prinzip.

Das Beispiel Nr. 4 ist mehr oder weniger dasselbe - es bettet die UI-Logik in die Datenschicht ein. Wenn wir nun korrigieren möchten, was der Benutzer sehen möchte, wenn keine Adresse vorhanden ist, müssen wir das Objekt in der Datenebene korrigieren. Und wenn zwei Projekte dasselbe Objekt verwenden, aber unterschiedlichen Text für die Nulladresse verwenden müssen?

Ich bin damit einverstanden, dass wenn wir "Tell, Don't Ask" für alles implementieren können, es sehr nützlich wäre - ich selbst würde mich freuen, wenn ich im wirklichen Leben nur erzählen kann, anstatt zu fragen (und es selbst zu tun)! Wie im echten Leben ist die Machbarkeit der Lösung jedoch stark auf High-Level-Klassen beschränkt.

tia
quelle