it-swarm.com.de

Builder-Muster: Wann sollte ein Fehler auftreten?

Bei der Implementierung des Builder-Musters bin ich oft verwirrt darüber, wann das Erstellen fehlschlagen soll, und es gelingt mir sogar, alle paar Tage unterschiedliche Standpunkte zu vertreten.

Zuerst eine Erklärung:

  • Mit früh fehlgeschlagen Ich meine, dass das Erstellen eines Objekts fehlschlagen sollte, sobald ein ungültiger Parameter übergeben wird. Also innerhalb von SomeObjectBuilder.
  • Mit spät fehlgeschlagen Ich meine, dass das Erstellen eines Objekts nur beim Aufruf von build() fehlschlagen kann, der implizit einen Konstruktor des zu erstellenden Objekts aufruft.

Dann einige Argumente:

  • Für ein spätes Scheitern: Eine Builder-Klasse sollte nicht mehr als eine Klasse sein, die einfach Werte enthält. Darüber hinaus führt dies zu weniger Codeduplizierungen.
  • Für ein frühzeitiges Scheitern: Ein allgemeiner Ansatz bei der Softwareprogrammierung besteht darin, dass Sie Probleme so früh wie möglich erkennen möchten. Daher ist der logischste Ort, den Sie überprüfen sollten, die Builder-Klasse 'Konstruktor', 'Setter' und letztendlich die Build-Methode.

Was ist die allgemeine Übereinstimmung darüber?

47
skiwi

Schauen wir uns die Optionen an, in denen wir den Validierungscode platzieren können:

  1. Innerhalb der Setter im Builder.
  2. Innerhalb der Methode build().
  3. Innerhalb der erstellten Entität: Sie wird in der Methode build() aufgerufen, wenn die Entität erstellt wird.

Option 1 ermöglicht es uns, Probleme früher zu erkennen, aber es kann komplizierte Fälle geben, in denen wir Eingaben nur mit dem vollständigen Kontext validieren können und somit zumindest einen Teil davon ausführen der Validierung in der Methode build(). Die Auswahl von Option 1 führt daher zu inkonsistentem Code, wobei ein Teil der Validierung an einer Stelle und ein anderer Teil an einer anderen Stelle durchgeführt wird.

Option 2 ist nicht wesentlich schlechter als Option 1, da Setter im Builder normalerweise direkt vor build() aufgerufen werden, insbesondere in fließenden Schnittstellen. Daher ist es in den meisten Fällen immer noch möglich, ein Problem früh genug zu erkennen. Wenn der Builder jedoch nicht die einzige Möglichkeit ist, ein Objekt zu erstellen, führt dies zu einer Verdoppelung des Validierungscodes, da Sie ihn überall dort haben müssen, wo Sie ein Objekt erstellen. Die logischste Lösung in diesem Fall besteht darin, die Validierung so nah wie möglich an das erstellte Objekt, dh innerhalb des Objekts, zu bringen. Und dies ist die Option 3 .

Aus der Sicht von SOLID] verstößt das Einfügen der Validierung in den Builder auch gegen SRP: Die Builder-Klasse ist bereits dafür verantwortlich, die Daten zu aggregieren, um ein Objekt zu erstellen. Bei der Validierung werden Verträge in ihrem eigenen internen Status erstellt neue Verantwortung, den Status eines anderen Objekts zu überprüfen.

Aus meiner Sicht ist es daher nicht nur besser, aus Sicht des Designs zu spät zu versagen, sondern es ist auch besser, innerhalb der konstruierten Entität zu versagen, als im Builder selbst.

UPD: dieser Kommentar erinnerte mich an eine weitere Möglichkeit, wenn die Validierung im Builder (Option 1 oder 2) sinnvoll ist. Es ist sinnvoll, wenn der Builder eigene Verträge für die von ihm erstellten Objekte hat. Angenommen, wir haben einen Builder, der eine Zeichenfolge mit einem bestimmten Inhalt erstellt, z. B. eine Liste von Nummernkreisen 1-2,3-4,5-6. Dieser Builder hat möglicherweise eine Methode wie addRange(int min, int max). Die resultierende Zeichenfolge weiß nichts über diese Zahlen und sollte es auch nicht wissen müssen. Der Builder selbst definiert das Format der Zeichenfolge und die Einschränkungen für die Zahlen. Daher muss die Methode addRange(int,int) die Eingabenummern validieren und eine Ausnahme auslösen, wenn max kleiner als min ist.

Die allgemeine Regel lautet jedoch, nur die vom Bauherrn selbst definierten Verträge zu validieren.

35
Ivan Gammel

Wenn Sie Java verwenden, beachten Sie die maßgeblichen und detaillierten Anleitungen von Joshua Bloch im Artikel Erstellen und Zerstören Java Objekte (Fettdruck im folgenden Zitat gehört mir):

Wie ein Konstruktor kann ein Builder seinen Parametern Invarianten auferlegen. Die Erstellungsmethode kann diese Invarianten überprüfen. Es ist wichtig, dass sie nach dem Kopieren der Parameter vom Builder in das Objekt überprüft werden und dass sie in den Objektfeldern und nicht in den Builderfeldern überprüft werden . (Punkt 39). Wenn Invarianten verletzt werden, sollte die Erstellungsmethode ein IllegalStateException (Item 60) auslösen. Die Detailmethode der Ausnahme sollte angeben, welche Invariante verletzt wird (Punkt 63).

Eine andere Möglichkeit, Invarianten mit mehreren Parametern aufzuerlegen, besteht darin, dass Setter-Methoden ganze Gruppen von Parametern übernehmen, für die einige Invarianten gelten müssen. Wenn die Invariante nicht erfüllt ist, löst die Setter-Methode ein IllegalArgumentException aus. Dies hat den Vorteil, dass der invariante Fehler erkannt wird, sobald die ungültigen Parameter übergeben werden, anstatt auf den Aufruf des Builds zu warten.

Hinweis gemäß Erklärung des Herausgebers in diesem Artikel beziehen sich "Elemente" im obigen Zitat auf Regeln, die in Effective Java, Second Edition vorgestellt werden.

Der Artikel geht nicht tief in die Erklärung ein, warum dies empfohlen wird, aber wenn Sie daran denken, sind die Gründe ziemlich offensichtlich. Ein allgemeiner Tipp zum Verständnis dieses Themas finden Sie genau dort im Artikel in der Erklärung, wie das Builder-Konzept mit dem des Konstruktors verbunden ist. Es wird erwartet, dass Klasseninvarianten im Konstruktor überprüft werden, nicht in einem anderen Code, der dem Aufruf vorausgeht oder diesen vorbereitet.

Betrachten Sie ein beliebtes Beispiel für CarBuilder , um ein konkreteres Verständnis dafür zu erhalten, warum das Überprüfen von Invarianten vor dem Aufrufen eines Builds falsch ist. Builder-Methoden können in beliebiger Reihenfolge aufgerufen werden. Daher kann man bis zum Build nicht wirklich wissen, ob ein bestimmter Parameter gültig ist.

Bedenken Sie, dass Sportwagen nicht mehr als 2 Sitze haben können. Wie kann man wissen, ob setSeats(4) in Ordnung ist oder nicht? Erst beim Build kann man sicher wissen, ob setSportsCar() aufgerufen wurde oder nicht, was bedeutet, ob TooManySeatsException geworfen werden soll oder nicht.

34
gnat

Ungültige Werte, die ungültig sind, weil sie nicht toleriert werden, sollten meiner Meinung nach sofort bekannt gegeben werden. Mit anderen Worten, wenn Sie nur positive Zahlen akzeptieren und eine negative Zahl übergeben wird, müssen Sie nicht warten, bis build() aufgerufen wird. Ich würde dies nicht als die Art von Problemen betrachten, die Sie "erwarten" würden, da dies eine Voraussetzung für den Aufruf der Methode ist. Mit anderen Worten, Sie würden wahrscheinlich nicht abhängig davon sein, dass bestimmte Parameter nicht eingestellt wurden. Es ist wahrscheinlicher, dass Sie davon ausgehen, dass die Parameter korrekt sind, oder dass Sie selbst eine Überprüfung durchführen.

Bei komplizierteren Problemen, die nicht so einfach zu validieren sind, ist es möglicherweise besser, wenn Sie build() aufrufen. Ein gutes Beispiel hierfür ist die Verwendung der von Ihnen angegebenen Verbindungsinformationen, um eine Verbindung zu einer Datenbank herzustellen. In diesem Fall ist es nicht mehr intuitiv und kompliziert nur Ihren Code, obwohl Sie technisch könnten nach solchen Bedingungen suchen. Aus meiner Sicht sind dies auch die Arten von Problemen, die tatsächlich auftreten können und die Sie erst dann wirklich vorhersehen können, wenn Sie es versuchen. Es ist eine Art Unterschied zwischen dem Abgleichen eines Strings mit einem regulären Ausdruck, um festzustellen, ob er könnte als int analysiert wird, und dem einfachen Versuch, ihn zu analysieren, wobei mögliche Ausnahmen behandelt werden, die als Folge auftreten können.

Ich mag es im Allgemeinen nicht, Ausnahmen zu setzen, wenn Parameter festgelegt werden, da dies bedeutet, dass jede ausgelöste Ausnahme abgefangen werden muss. Daher bevorzuge ich die Validierung in build(). Aus diesem Grund bevorzuge ich die Verwendung von RuntimeException, da Fehler in übergebenen Parametern im Allgemeinen nicht auftreten sollten.

Dies ist jedoch mehr als alles andere eine bewährte Methode. Ich hoffe das beantwortet deine Frage.

19
Neil

Soweit ich weiß, besteht die allgemeine Praxis (nicht sicher, ob Konsens besteht) darin, so früh wie möglich zu scheitern, um einen Fehler zu entdecken. Dies macht es auch schwieriger, Ihre API unbeabsichtigt zu missbrauchen.

Wenn es sich um ein triviales Attribut handelt, das bei der Eingabe überprüft werden kann, z. B. eine Kapazität oder Länge, die nicht negativ sein sollte, sollten Sie sofort fehlschlagen. Wenn Sie den Fehler zurückhalten, vergrößert sich der Abstand zwischen Fehler und Rückmeldung, wodurch es schwieriger wird, die Ursache des Problems zu finden.

Wenn Sie das Unglück haben, in einer Situation zu sein, in der die Gültigkeit eines Attributs von anderen abhängt, haben Sie zwei Möglichkeiten:

  • Erfordern, dass beide (oder mehr) Attribute gleichzeitig angegeben werden (d. H. Aufruf einer einzelnen Methode).
  • Testen Sie die Gültigkeit, sobald Sie wissen, dass keine Änderungen mehr eingehen: wenn build() oder so aufgerufen wird.

Wie bei den meisten Dingen ist dies eine Entscheidung, die in einem Kontext getroffen wird. Wenn der Kontext es schwierig oder kompliziert macht, frühzeitig zu versagen, kann ein Kompromiss geschlossen werden, um Überprüfungen auf einen späteren Zeitpunkt zu verschieben. Fail-Fast sollte jedoch die Standardeinstellung sein.

11
JvR

Die Grundregel lautet "früh scheitern".

Die etwas fortgeschrittenere Regel lautet "so früh wie möglich scheitern".

Wenn eine Eigenschaft intrinsisch ungültig ist ...

CarBuilder.numberOfWheels( -1 ). ...  

... dann lehnen Sie es sofort ab.

In anderen Fällen müssen möglicherweise Werte überprüft werden in Kombination und möglicherweise besser in der build () -Methode platziert werden:

CarBuilder.numberOfWheels( 0 ).type( 'Hovercraft' ). ...  
0
Phill W.