Ich habe viele Implementierungen des Builder-Musters gesehen (hauptsächlich in Java). Alle von ihnen haben eine Entity-Klasse (sagen wir eine Person
Klasse) und eine Builder-Klasse PersonBuilder
. Der Builder "stapelt" verschiedene Felder und gibt ein new Person
mit den übergebenen Argumenten zurück. Warum benötigen wir explizit eine Builder-Klasse, anstatt alle Builder-Methoden in der Person
Klasse selbst zu platzieren?
Beispielsweise:
class Person {
private String name;
private Integer age;
public Person() {
}
Person withName(String name) {
this.name = name;
return this;
}
Person withAge(int age) {
this.age = age;
return this;
}
}
Ich kann es einfach sagen Person john = new Person().withName("John");
Warum braucht man eine PersonBuilder
Klasse?
Der einzige Vorteil, den ich sehe, ist, dass wir die Person
Felder als deklarieren können, um die final
Unveränderlichkeit zu gewährleisten.
java
design-patterns
builder-pattern
Boyan Kushlev
quelle
quelle
chainable setters
: DwithName
eine Kopie der Person zurücksenden, wobei nur das Namensfeld geändert wurde. Mit anderen Worten,Person john = new Person().withName("John");
könnte funktionieren, auch wennPerson
unveränderlich ist (und dies ist ein verbreitetes Muster in der funktionalen Programmierung).void
Methoden. Wenn beispielsweisePerson
eine Methode ihren Namen ausgibt, können Sie sie dennoch mit einer Fluent-Schnittstelle verkettenperson.setName("Alice").sayName().setName("Bob").sayName()
. Übrigens kommentiere ich die in JavaDoc mit genau Ihrem Vorschlag@return Fluent interface
- es ist allgemein und klar genug, wenn es auf eine Methode angewendet wird, diereturn this
am Ende ihrer Ausführung ausgeführt wird, und es ist ziemlich klar. Ein Builder wird also auch eine flüssige Benutzeroberfläche erstellen.Antworten:
So können Sie unveränderlich sein UND gleichzeitig benannte Parameter simulieren .
So bleiben Ihre Finger von der Person fern, bis der Status festgelegt ist, und Sie können ihn nach dem Festlegen nicht mehr ändern, obwohl jedes Feld eindeutig gekennzeichnet ist. Sie können dies nicht mit nur einer Klasse in Java tun.
Sie sprechen anscheinend über Josh Blochs Builder Pattern . Dies sollte nicht mit dem Builder-Muster der Viererbande verwechselt werden . Das sind verschiedene Tiere. Sie lösen beide Bauprobleme, aber auf ziemlich unterschiedliche Weise.
Natürlich können Sie Ihr Objekt auch ohne Verwendung einer anderen Klasse erstellen. Aber dann muss man sich entscheiden. Sie verlieren entweder die Fähigkeit, benannte Parameter in Sprachen zu simulieren, die diese nicht haben (wie Java), oder Sie verlieren die Fähigkeit, während der gesamten Lebensdauer des Objekts unveränderlich zu bleiben.
Unveränderliches Beispiel, hat keine Namen für Parameter
Hier erstellen Sie alles mit einem einzigen einfachen Konstruktor. Dadurch bleiben Sie unveränderlich, verlieren jedoch die Simulation der benannten Parameter. Das ist bei vielen Parametern schwer zu lesen. Computer kümmern sich nicht darum, aber es ist hart für die Menschen.
Beispiel für einen simulierten benannten Parameter mit herkömmlichen Setzern. Nicht unveränderlich.
Hier erstellen Sie alles mit Setzern und simulieren benannte Parameter, aber Sie sind nicht mehr unveränderlich. Jede Verwendung eines Setters ändert den Objektstatus.
Durch Hinzufügen der Klasse erhalten Sie also die Möglichkeit, beides zu tun.
Die Validierung kann in durchgeführt werden,
build()
wenn ein Laufzeitfehler für ein fehlendes Altersfeld für Sie ausreicht. Sie können das aktualisieren und das erzwingen,age()
das mit einem Compiler-Fehler aufgerufen wird. Nur nicht mit dem Josh Bloch Builder Pattern.Dafür benötigen Sie eine interne Domain Specific Language (iDSL).
So können Sie verlangen, dass sie anrufen
age()
undname()
bevor Sie anrufenbuild()
. Aber Sie können es nicht einfach tun, indem Siethis
jedes Mal zurückkehren. Jedes zurückgegebene Objekt gibt ein anderes Objekt zurück, das Sie zwingt, das nächste Objekt aufzurufen.Die Verwendung könnte folgendermaßen aussehen:
Aber dieses:
verursacht einen Compilerfehler, da
age()
nur der Typ gültig ist, der von zurückgegeben wirdname()
.Diese iDSLs sind extrem leistungsfähig ( z. B. JOOQ- oder Java8-Streams ) und sehr benutzerfreundlich , insbesondere, wenn Sie eine IDE mit Code-Vervollständigung verwenden, die Einrichtung ist jedoch aufwändig . Ich würde empfehlen, sie für Dinge zu speichern, gegen die einiges an Quellcode geschrieben wird.
quelle
AnonymousPersonBuilder
, die ihre eigenen Regeln hat.Gründe für die Verwendung / Bereitstellung einer Builder-Klasse:
quelle
PersonBuilder
keine Getter hat, und die einzige Möglichkeit, die aktuellen Werte zu überprüfen, besteht darin, aufzurufen.Build()
, um a zurückzugebenPerson
. Auf diese Weise kann .Build ein ordnungsgemäß erstelltes Objekt validieren, richtig? Dh dies ist der Mechanismus, der die Verwendung eines "im Bau befindlichen" Objekts verhindern soll?Build
Operation eingefügt werden , um fehlerhafte und / oder unterkonstruierte Objekte zu verhindern.getAge
erforderlich. Wenn ein Builder über Getter verfügt, müssen Sie sich darüber im Klaren sein, dass der Aufruf des Builders möglicherweise zurückgegeben wird,null
wenn diese Eigenschaft noch nicht angegeben wurde. Im Gegensatz dazu kann diePerson
Klasse die Invariante erzwingen, dass Alter niemals sein kannnull
, was unmöglich ist, wenn Builder und tatsächliches Objekt gemischt werden, wie imnew Person() .withName("John")
Beispiel des OP , das glücklichPerson
ein Alter ohne Alter erzeugt. Dies gilt auch für veränderbare Klassen, da Setter zwar Invarianten, aber keine Anfangswerte erzwingen können.Ein Grund wäre, sicherzustellen, dass alle übergebenen Daten den Geschäftsregeln entsprechen.
In Ihrem Beispiel wird dies nicht berücksichtigt. Nehmen wir jedoch an, dass jemand eine leere Zeichenfolge oder eine Zeichenfolge aus Sonderzeichen übergeben hat. Sie sollten eine Art Logik verwenden, um sicherzustellen, dass der Name tatsächlich gültig ist (was eigentlich eine sehr schwierige Aufgabe ist).
Sie könnten das alles in Ihre Person-Klasse aufnehmen, besonders wenn die Logik sehr klein ist (zum Beispiel nur sicherstellen, dass ein Alter nicht negativ ist), aber wenn die Logik wächst, ist es sinnvoll, sie zu trennen.
quelle
john
Ein bisschen anders, als ich es in anderen Antworten sehe.
Der
withFoo
Ansatz hier ist problematisch, da sie sich wie Setter verhalten, aber so definiert sind, dass es so aussieht, als ob die Klasse die Unveränderlichkeit unterstützt. Wenn eine Methode in Java-Klassen eine Eigenschaft ändert, ist es üblich, die Methode mit 'set' zu starten. Ich habe es als Standard noch nie geliebt, aber wenn Sie etwas anderes tun, wird es die Leute überraschen und das ist nicht gut. Es gibt eine andere Möglichkeit, die Unveränderlichkeit mit der hier verfügbaren Basis-API zu unterstützen. Beispielsweise:Es bietet nicht viel, um nicht ordnungsgemäß erstellte Objekte zu verhindern, verhindert jedoch Änderungen an vorhandenen Objekten. Es ist wahrscheinlich albern für so etwas (und so ist der JB Builder). Ja, Sie werden mehr Objekte erstellen, aber das ist nicht so teuer, wie Sie vielleicht denken.
Diese Vorgehensweise wird meistens bei gleichzeitigen Datenstrukturen wie CopyOnWriteArrayList verwendet . Und dies deutet darauf hin, warum Unveränderlichkeit wichtig ist. Wenn Sie Ihren Code threadsicher machen möchten, sollte die Unveränderlichkeit fast immer berücksichtigt werden. In Java darf jeder Thread einen lokalen Cache mit variablem Status führen. Damit ein Thread die in anderen Threads vorgenommenen Änderungen sehen kann, muss ein synchronisierter Block oder eine andere Nebenläufigkeitsfunktion verwendet werden. Jede dieser Methoden erhöht den Aufwand für den Code. Aber wenn Ihre Variablen endgültig sind, gibt es nichts zu tun. Der Wert wird immer so sein, wie er initialisiert wurde, und daher sehen alle Threads das Gleiche, egal was passiert.
quelle
Ein weiterer Grund, der hier nicht explizit erwähnt wurde, ist, dass die
build()
Methode überprüfen kann, ob alle Felder 'Felder sind, die gültige Werte enthalten (entweder direkt festgelegt oder von anderen Werten anderer Felder abgeleitet). Dies ist wahrscheinlich der wahrscheinlichste Fehlermodus das würde sonst passieren.Ein weiterer Vorteil ist, dass Ihr
Person
Objekt eine einfachere Lebensdauer und eine einfachere Menge von Invarianten hat. Sie wissen, dass SiePerson p
einp.name
und ein gültiges haben, sobald Sie ein habenp.age
. Keine Ihrer Methoden muss so konzipiert sein, dass sie Situationen wie "Nun, was ist, wenn das Alter festgelegt ist, aber kein Name, oder was ist, wenn der Name festgelegt ist, aber kein Alter?" Dies reduziert die Gesamtkomplexität der Klasse.quelle
URI
, aFile
oder a konfiguriertFileInputStream
und verwendet alles, was bereitgestellt wurde, um a zu erhaltenFileInputStream
, das schließlich als argEin Builder kann auch so definiert werden, dass er eine Schnittstelle oder eine abstrakte Klasse zurückgibt. Mit dem Builder können Sie das Objekt definieren, und der Builder kann festlegen, welche konkrete Unterklasse zurückgegeben werden soll, z. B. basierend auf den festgelegten Eigenschaften oder den festgelegten Eigenschaften.
quelle
Mit dem Builder-Muster wird das Objekt Schritt für Schritt erstellt, indem die Eigenschaften festgelegt werden. Wenn alle erforderlichen Felder festgelegt sind, wird das endgültige Objekt mithilfe der Erstellungsmethode zurückgegeben. Das neu erstellte Objekt ist unveränderlich. Hier ist vor allem zu beachten, dass das Objekt nur zurückgegeben wird, wenn die endgültige Erstellungsmethode aufgerufen wird. Dadurch wird sichergestellt, dass alle Eigenschaften für das Objekt festgelegt sind und sich das Objekt daher nicht im inkonsistenten Zustand befindet, wenn es von der Builder-Klasse zurückgegeben wird.
Wenn wir die Builder-Klasse nicht verwenden und alle Builder-Klassenmethoden direkt der Person-Klasse selbst zuweisen, müssen wir zuerst das Objekt erstellen und dann die Setter-Methoden für das erstellte Objekt aufrufen, was zu einem inkonsistenten Zustand des Objekts zwischen der Erstellung führt von Objekt und Festlegen der Eigenschaften.
Durch die Verwendung der Builder-Klasse (dh einer anderen externen Entität als der Person-Klasse selbst) stellen wir sicher, dass sich das Objekt niemals im inkonsistenten Zustand befindet.
quelle
Builder-Objekt wiederverwenden
Wie bereits erwähnt, sind die Unveränderlichkeit und die Überprüfung der Geschäftslogik aller Felder zur Validierung des Objekts die Hauptgründe für ein separates Builder-Objekt.
Die Wiederverwendbarkeit ist jedoch ein weiterer Vorteil. Wenn ich viele sehr ähnliche Objekte instanziieren möchte, kann ich kleine Änderungen am Builder-Objekt vornehmen und mit der Instantiierung fortfahren. Das Builder-Objekt muss nicht neu erstellt werden. Durch diese Wiederverwendung kann der Builder als Vorlage zum Erstellen vieler unveränderlicher Objekte fungieren. Dies ist ein kleiner Vorteil, der jedoch nützlich sein kann.
quelle
Eigentlich Sie können die Builder - Methoden auf Ihre Klasse haben sich, und noch Unveränderlichkeit haben. Dies bedeutet lediglich, dass die Builder-Methoden neue Objekte zurückgeben, anstatt das vorhandene zu ändern.
Dies funktioniert nur, wenn es eine Möglichkeit gibt, ein erstes (gültiges / nützliches) Objekt zu erhalten (z. B. von einem Konstruktor, der alle erforderlichen Felder festlegt, oder einer Factory-Methode, die Standardwerte festlegt). Die zusätzlichen Builder-Methoden geben dann geänderte Objekte basierend zurück auf der bestehenden. Diese Builder-Methoden müssen sicherstellen, dass Sie unterwegs keine ungültigen / inkonsistenten Objekte erhalten.
Das bedeutet natürlich, dass Sie viele neue Objekte haben werden, und Sie sollten dies nicht tun, wenn die Erstellung Ihrer Objekte teuer ist.
Ich habe das im Testcode verwendet, um Hamcrest-Matcher für eines meiner Geschäftsobjekte zu erstellen . Ich erinnere mich nicht an den genauen Code, aber er sah ungefähr so aus (vereinfacht):
Ich würde es dann in meinen Unit-Tests so verwenden (mit geeigneten statischen Importen):
quelle
name
aber ohneage
), funktioniert das Konzept nicht. Nun, Sie könnten tatsächlich so etwas zurückgeben,PartiallyBuiltPerson
während es nicht gültig ist, aber es scheint ein Hack zu sein, um den Builder zu maskieren.