Prinzip des geringsten Erstaunens (POLA) und Schnittstellen

17

Vor gut einem Vierteljahrhundert, als ich C ++ lernte, wurde mir beigebracht, dass Interfaces verzeihen sollten und dass die Reihenfolge, in der Methoden aufgerufen wurden, so weit wie möglich unberücksichtigt bleiben sollte, da der Verbraucher möglicherweise keinen Zugriff auf die Quelle oder Dokumentation anstelle von hat Dies.

Wann immer ich Junior-Programmierer betreut habe und Senior-Entwickler mich belauscht haben, haben sie mit Erstaunen reagiert, was mich gefragt hat, ob dies wirklich eine Sache war oder ob sie gerade aus der Mode gekommen ist.

So klar wie Schlamm?

Betrachten Sie eine Schnittstelle mit diesen Methoden (zum Erstellen von Datendateien):

OpenFile
SetHeaderString
WriteDataLine
SetTrailerString
CloseFile

Jetzt können Sie diese natürlich der Reihe nach durchgehen, aber sagen, dass Sie sich nicht um den Dateinamen (think a.out) gekümmert haben oder welche Header- und Trailer-Zeichenfolge enthalten waren, Sie können sie einfach aufrufen AddDataLine.

Ein weniger extremes Beispiel könnte das Weglassen von Headern und Trailern sein.

Eine weitere Möglichkeit besteht darin, die Header- und Trailer-Zeichenfolgen festzulegen, bevor die Datei geöffnet wurde.

Handelt es sich um ein Prinzip des Interface-Designs, das erkannt wurde, oder nur um das POLA-Prinzip, bevor es einen Namen erhielt?

Hinweis: Lassen Sie sich in den Details dieser Benutzeroberfläche nicht stören. Dies ist nur ein Beispiel für diese Frage.

Robbie Dee
quelle
10
Das Prinzip "geringstes Erstaunen" ist beim Entwurf von Benutzeroberflächen weitaus häufiger anzutreffen als beim Entwurf von "Anwendungsprogrammierschnittstellen". Der Grund dafür ist, dass von einem Benutzer einer Website oder eines Programms überhaupt nicht erwartet werden kann, dass er Anweisungen liest, bevor er sie verwendet, während von einem Programmierer zumindest im Prinzip erwartet wird, dass er die API-Dokumente liest, bevor er mit ihnen programmiert.
Kilian Foth
7
@KilianFoth: Ich bin mir ziemlich sicher, dass Wikipedia diesbezüglich falsch ist. POLA befasst sich nicht nur mit dem Design von Benutzeroberflächen, der Begriff "Prinzip der geringsten Überraschung" (der ziemlich gleich ist) wird auch von Bob Martin für Funktion und Klassendesign in seinem verwendet "Clean Code" Buch.
Doc Brown
2
Oft ist eine unveränderbare Schnittstelle ohnehin besser. Sie können alle Daten angeben, die Sie zum Zeitpunkt der Erstellung festlegen möchten. Keine Mehrdeutigkeiten mehr und die Klasse wird einfacher zu schreiben. (Manchmal ist dieses Schema natürlich nicht möglich.)
usr
4
Stimme überhaupt nicht zu, dass POLA nicht für APIs gilt. Es gilt für alles, was ein Mensch für andere Menschen erschafft. Wenn sich die Dinge wie erwartet verhalten, sind sie einfacher zu konzipieren und verursachen daher eine geringere kognitive Belastung, sodass die Menschen mit weniger Aufwand mehr Dinge tun können.
Gort the Robot

Antworten:

25

Eine Möglichkeit, sich an das Prinzip des geringsten Erstaunens zu halten, besteht darin, andere Prinzipien wie ISP und SRP oder sogar DRY zu berücksichtigen .

In dem konkreten Beispiel, das Sie angegeben haben, scheint der Vorschlag zu sein, dass es eine gewisse Abhängigkeit der Reihenfolge für die Bearbeitung der Datei gibt. Ihre API kontrolliert jedoch sowohl den Dateizugriff als auch das Datenformat, was ein bisschen nach einer Verletzung von SRP riecht.

Bearbeiten / Aktualisieren: Es wird auch vorgeschlagen, dass die API den Benutzer auffordert, DRY zu verletzen, da er bei jeder Verwendung der API dieselben Schritte wiederholen muss .

Stellen Sie sich eine alternative API vor, bei der die E / A-Vorgänge von den Datenvorgängen getrennt sind. und wo die API selbst die Bestellung "besitzt":

ContentBuilder

SetHeader( ... )
AddLine( ... )
SetTrailer ( ... )

FileWriter

Open(filename) 
Write(content) throws InvalidContentException
Close()

Mit der obigen Trennung muss das ContentBuilderProgramm nichts weiter tun, als die Zeilen / Header / Trailer zu speichern (möglicherweise auch eine ContentBuilder.Serialize()Methode, die die Reihenfolge kennt). Wenn Sie anderen SOLID-Prinzipien folgen, spielt es keine Rolle mehr, ob Sie den Header oder den Trailer vor oder nach dem Hinzufügen von Zeilen setzen, da nichts in die ContentBuilderDatei geschrieben wird, bis es an übergeben wird FileWriter.Write.

Es hat auch den zusätzlichen Vorteil, dass es etwas flexibler ist. Beispielsweise kann es nützlich sein, den Inhalt in einen Diagnose-Logger zu schreiben oder ihn über ein Netzwerk weiterzuleiten, anstatt ihn direkt in eine Datei zu schreiben.

Beim Entwerfen einer API sollten Sie auch die Fehlerberichterstattung berücksichtigen, unabhängig davon, ob es sich um einen Status, einen Rückgabewert, eine Ausnahme, einen Rückruf oder etwas anderes handelt. Der Benutzer der API wird wahrscheinlich erwarten können, dass er Verstöße gegen seine Verträge oder sogar andere Fehler, die er nicht kontrollieren kann, wie z. B. Datei-E / A-Fehler, programmgesteuert erkennen kann.

Ben Cottrell
quelle
Genau das, wonach ich gesucht habe - danke! Aus dem ISP-Artikel: "(ISP) besagt, dass kein Client gezwungen werden sollte, sich auf Methoden zu verlassen, die er nicht verwendet"
Robbie Dee
5
Dies ist keine schlechte Antwort, dennoch kann der Content Builder in einer Weise implementiert werden, in der die Reihenfolge der Aufrufe von SetHeaderoder von Bedeutung ist AddLine. Um diese Auftragsabhängigkeit zu beseitigen, handelt es sich weder um ISP noch um SRP, sondern lediglich um POLA.
Doc Brown
Wenn es auf die Bestellung ankommt, können Sie POLA dennoch erfüllen, indem Sie die Vorgänge so definieren, dass für die Ausführung späterer Schritte ein Wert erforderlich ist, der von früheren Schritten zurückgegeben wurde, wodurch die Bestellung mit dem Typsystem erzwungen wird. FileWriterIn diesem Fall kann der Wert aus dem letzten ContentBuilderSchritt der WriteMethode erforderlich sein , um sicherzustellen, dass der gesamte Eingabeinhalt vollständig ist, sodass er InvalidContentExceptionnicht erforderlich ist .
Dan Lyons
@DanLyons Ich denke, das ist ziemlich nah an der Situation, die der Fragesteller zu vermeiden versucht. wo der Benutzer der API die Bestellung kennen oder sich darum kümmern muss. Idealerweise sollte die API selbst die Reihenfolge erzwingen, andernfalls wird der Benutzer möglicherweise aufgefordert, DRY zu verletzen. Das ist der Grund , dieses Wissen aufzuteilen ContentBuilderund zusammenzufassen FileWriter.Write. Die Ausnahme wäre notwendig, falls irgendetwas mit dem Inhalt nicht übereinstimmt (z. B. ein fehlender Header). Eine Rückkehr könnte auch funktionieren, aber ich bin kein Fan davon, Ausnahmen in Rückkehrcodes umzuwandeln.
Ben Cottrell
Aber es lohnt sich auf jeden Fall, der Antwort weitere Notizen zu DRY und der Bestellung hinzuzufügen.
Ben Cottrell
12

Hier geht es nicht nur um POLA, sondern auch darum, einen ungültigen Status als mögliche Fehlerquelle zu verhindern.

Lassen Sie uns sehen, wie wir Ihrem Beispiel einige Einschränkungen auferlegen können, ohne eine konkrete Implementierung bereitzustellen:

Erster Schritt: Lassen Sie keinen Aufruf zu, bevor eine Datei geöffnet wurde.

CreateDataFileInterface
  + OpenFile(filename : string) : DataFileInterface

DataFileInterface
  + SetHeaderString(header : string) : void
  + WriteDataLine(data : string) : void
  + SetTrailerString(trailer : string) : void
  + Close() : void

Nun sollte klar sein, dass CreateDataFileInterface.OpenFilezum Abrufen einer DataFileInterfaceInstanz aufgerufen werden muss , in die die eigentlichen Daten geschrieben werden können.

Zweiter Schritt: Stellen Sie sicher, dass die Header und Trailer immer gesetzt sind.

CreateDataFileInterface
  + OpenFile(filename : string, header: string, trailer : string) : DataFileInterface

DataFileInterface
  + WriteDataLine(data : string) : void
  + Close() : void

Jetzt müssen Sie alle erforderlichen Parameter angeben, um einen DataFileInterfaceDateinamen, einen Header und einen Trailer zu erhalten. Wenn der Trailer-String nicht verfügbar ist, bis alle Zeilen geschrieben sind, können Sie diesen Parameter auch in verschieben Close()(möglicherweise in umbenennen WriteTrailerAndClose()), sodass die Datei zumindest nicht ohne Trailer-String fertiggestellt werden kann.


Um auf den Kommentar zu antworten:

Ich mag die Trennung der Schnittstelle. Ich bin jedoch der Meinung, dass Ihr Vorschlag zur Durchsetzung (z. B. WriteTrailerAndClose ()) einer Verletzung von SRP nahe kommt. (Dies ist etwas, mit dem ich mehrmals zu kämpfen hatte, aber Ihr Vorschlag scheint ein mögliches Beispiel zu sein.) Wie würden Sie antworten?

Wahr. Ich wollte mich nicht mehr auf das Beispiel konzentrieren als nötig, um meinen Standpunkt darzulegen, aber es ist eine gute Frage. In diesem Fall denke ich, ich würde es nennen Finalize(trailer)und argumentieren, dass es nicht zu viel tut. Das Schreiben des Trailers und das Schließen sind lediglich Implementierungsdetails. Aber wenn Sie anderer Meinung sind oder eine ähnliche Situation haben, in der es anders ist, ist hier eine mögliche Lösung:

CreateDataFileInterface
  + OpenFile(filename : string, header : string) : IncompleteDataFileInterface

IncompleteDataFileInterface
  + WriteDataLine(data : string) : void
  + FinalizeWithTrailer(trailer : string) : CompleteDataFileInterface

CompleteDataFileInterface
  + Close()

Ich würde es für dieses Beispiel eigentlich nicht tun, aber es zeigt, wie man die Technik konsequent durchführt.

Übrigens habe ich angenommen, dass die Methoden tatsächlich in dieser Reihenfolge aufgerufen werden müssen, um beispielsweise viele Zeilen nacheinander zu schreiben. Wenn dies nicht erforderlich ist, würde ich immer einen Baumeister bevorzugen, wie von Ben Cottrel vorgeschlagen .

Fabian Schmengler
quelle
1
Sie sind leider in die Falle gegangen. Ich habe Sie ausdrücklich gewarnt, dies von Anfang an zu vermeiden. Ein Dateiname ist nicht erforderlich, ebenso wenig wie der Header und der Trailer. Aber das allgemeine Thema der Aufteilung der Schnittstelle ist gut so +1 :-)
Robbie Dee
Oh, dann habe ich Sie falsch verstanden, ich dachte, dies beschreibt die Absicht des Benutzers, nicht die Implementierung.
Fabian Schmengler
Ich mag die Trennung der Schnittstelle. Ich bin jedoch der Meinung, dass Ihr Vorschlag zur Durchsetzung (z. B. WriteTrailerAndClose()) einer Verletzung der SRP gleichkommt. (Dies ist etwas, mit dem ich mehrmals zu kämpfen hatte, aber Ihr Vorschlag scheint ein mögliches Beispiel zu sein.) Wie würden Sie antworten?
kmote
1
@ kmote Antwort war zu lang für einen Kommentar, siehe mein Update
Fabian Schmengler
1
Wenn der Dateiname optional ist, können Sie eine OpenFileÜberladung bereitstellen , für die keine erforderlich ist.
5gon12eder