Bei der Implementierung des Builder-Musters stelle ich häufig fest, wann das Erstellen fehlschlagen soll, und es gelingt mir sogar, alle paar Tage andere Standpunkte in dieser Angelegenheit zu vertreten.
Zunächst einige Erklärungen:
- Mit frühzeitig fehlschlagen meine ich, dass das Erstellen eines Objekts fehlschlagen sollte, sobald ein ungültiger Parameter übergeben wird
SomeObjectBuilder
. - Mit Verspätung meine ich, dass das Erstellen eines Objekts nur bei dem
build()
Aufruf fehlschlagen kann, der implizit einen Konstruktor des zu erstellenden Objekts aufruft.
Dann einige Argumente:
- Zu Gunsten eines verspäteten Scheiterns: Eine Builder-Klasse sollte nicht mehr als eine Klasse sein, die einfach Werte enthält. Darüber hinaus führt dies zu weniger Code-Duplikationen.
- Für ein frühzeitiges Scheitern: Ein allgemeiner Ansatz bei der Softwareprogrammierung besteht darin, Probleme so früh wie möglich zu erkennen. Daher sollte die Builder-Klasse "Konstruktor", "Setter" und letztendlich die Build-Methode am logischsten überprüft werden.
Was ist die allgemeine Übereinstimmung darüber?
java
design-patterns
Skiwi
quelle
quelle
null
Objekt , wenn es ein Problem inbuild()
.Antworten:
Schauen wir uns die Optionen an, in denen wir den Validierungscode platzieren können:
build()
Methode.build()
Methode 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, wodurch zumindest ein Teil der Validierung in der
build()
Methode durchgeführt wird. Daher führt die Auswahl von Option 1 zu inkonsistentem Code, wobei ein Teil der Validierung an einer Stelle und ein anderer Teil an einer anderen Stelle ausgeführt wird.Option 2 ist nicht wesentlich schlechter als Option 1, da die Setter in Builder normalerweise direkt vor den
build()
Interfaces aufgerufen werden , insbesondere bei fließenden Interfaces. Somit 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, wird der Validierungscode dupliziert, da Sie ihn überall dort benötigen, wo Sie ein Objekt erstellen. Die logischste Lösung besteht in diesem Fall darin, die Validierung so nah wie möglich am erstellten Objekt, dh innerhalb des Objekts, zu platzieren. Und das ist die Option 3 .Unter SOLID-Gesichtspunkten verstößt das Versetzen der Validierung in 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 für den eigenen internen Status erstellt. Es ist eine neue Verantwortung, den Status eines anderen Objekts zu überprüfen.
Aus meiner Sicht ist es daher nicht nur besser, aus Designsicht spät zu scheitern, sondern auch besser, innerhalb der konstruierten Entität zu scheitern, 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 Nummernbereichen
1-2,3-4,5-6
. Dieser Builder hat möglicherweise eine Methode wieaddRange(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 MethodeaddRange(int,int)
die eingegebenen Zahlen validieren und eine Ausnahme auslösen, wenn max kleiner als min ist.Die allgemeine Regel ist jedoch, nur die vom Bauherrn selbst definierten Verträge zu validieren.
quelle
Wenn Sie Java verwenden, beachten Sie die maßgeblichen und detaillierten Anleitungen von Joshua Bloch im Artikel Erstellen und Löschen von Java-Objekten (die fett gedruckte Schrift im folgenden Zitat stammt von mir):
Beachten Sie , dass sich "Elemente" in obigem Zitat laut der Erläuterung des Editors zu diesem Artikel auf Regeln beziehen, die in Effective Java, Second Edition, vorgestellt werden .
Der Artikel erklärt nicht ausführlich, warum dies empfohlen wird, aber wenn Sie darüber nachdenken, sind die Gründe ziemlich offensichtlich. Ein allgemeiner Tipp zum Verständnis dieses Problems finden Sie direkt 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 und nicht in einem anderen Code, der dem Aufruf vorangeht / ihn vorbereitet.
Um zu verstehen, warum das Prüfen von Invarianten vor dem Aufrufen eines Builds falsch ist, können Sie ein beliebtes Beispiel für CarBuilder betrachten . Builder-Methoden können in einer beliebigen Reihenfolge aufgerufen werden, und daher kann man nicht genau wissen, ob ein bestimmter Parameter bis zum Build gültig ist.
Wenn man bedenkt, dass ein Sportwagen nicht mehr als zwei Sitze haben kann, wie kann man dann wissen, ob
setSeats(4)
es in Ordnung ist oder nicht? Es ist nur am Aufbau, wenn man sicher wissen kann, obsetSportsCar()
aufgerufen wurde oder nicht, was bedeutet, ob zu werfenTooManySeatsException
oder nicht.quelle
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 der
build()
Anruf eingeht. Ich würde dies nicht als die Art von Problemen betrachten, von denen Sie "erwarten", dass sie auftreten, da dies eine Grundvoraussetzung für den Aufruf der Methode ist. Mit anderen Worten, Sie sind wahrscheinlich nicht darauf angewiesen , dass bestimmte Parameter nicht eingestellt werden. Es ist wahrscheinlicher, dass Sie davon ausgehen, dass die Parameter korrekt sind, oder dass Sie selbst eine Überprüfung durchführen würden.Bei komplizierteren Problemen, die nicht so einfach zu überprüfen sind, sollten Sie jedoch beim Anruf darauf aufmerksam machen
build()
. Ein gutes Beispiel hierfür ist die Verwendung der von Ihnen angegebenen Verbindungsinformationen, um eine Verbindung zu einer Datenbank herzustellen. In diesem Fall , während Sie technisch könnte für solche Bedingungen zu überprüfen, ist es nicht mehr intuitiv und es erschwert nur den Code. Aus meiner Sicht sind dies auch die Arten von Problemen, die tatsächlich auftreten können und die Sie erst dann wirklich antizipieren können, wenn Sie es versuchen. Es ist eine Art des Unterschiedes zwischen einem String mit einem regulären Ausdruck entsprechen , um zu sehen , ob es könnte als int analysiert werden und einfach versuchen , es zu analysieren, mögliche Behandlung von Ausnahmen , die als Folge auftreten können.Ich mag es im Allgemeinen nicht, beim Festlegen von Parametern Ausnahmen auszulösen, da dies bedeutet, dass jede ausgelöste Ausnahme abgefangen werden muss
build()
. Aus diesem Grund bevorzuge ich die Verwendung von RuntimeException, da Fehler in den übergebenen Parametern im Allgemeinen nicht auftreten sollten.Dies ist jedoch mehr als alles andere eine bewährte Methode. Ich hoffe das beantwortet deine Frage.
quelle
Soweit ich weiß, besteht die allgemeine Praxis (nicht sicher, ob Konsens besteht) darin, so früh wie möglich 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 darf, sollten Sie am besten sofort versagen. Wenn Sie den Fehler unterdrücken, vergrößert sich der Abstand zwischen Fehler und Feedback, 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:
build()
oder so wird aufgerufen.Wie bei den meisten Dingen ist dies eine Entscheidung, die in einem Kontext getroffen wird. Wenn der Kontext ein frühzeitiges Scheitern schwierig oder umständlich macht, kann ein Kompromiss geschlossen werden, um Überprüfungen auf einen späteren Zeitpunkt zu verschieben. Die Standardeinstellung lautet jedoch "Fehlerfrei".
quelle
unsigned
,@NonNull
etc.X
auf einen Wert festzulegen, der angesichts des aktuellen Werts von ungültigY
ist. aber vor dem Aufrufbuild()
SatzY
auf einen Wert, würdeX
gültig.Shape
und der Bauherr hatWithLeft
undWithRight
Eigenschaften, und man wünscht einen Builder anzupassen ein Objekt an einem anderen Ort zu konstruieren, zu verlangen , dassWithRight
zuerst aufgerufen werden , wenn ein Objekt rechts bewegt, undWithLeft
beim Bewegen sie, links unnötige Komplexität würde im Vergleich zu erlaubenWithLeft
, die linke Kante rechts von der alten rechten Kante zu setzen, vorausgesetzt, dassWithRight
die rechte Kante fixiert, bevorbuild
aufgerufen wird.Die Grundregel lautet "Früh scheitern".
Die etwas weiter fortgeschrittene Regel lautet "so früh wie möglich scheitern".
Wenn eine Eigenschaft ist intrinsisch ungültig ...
... dann lehnen Sie es sofort ab.
In anderen Fällen müssen die Werte möglicherweise in Kombination überprüft und besser in die build () -Methode eingefügt werden:
quelle