Bei der Verwendung von Methodenverkettungen wie:
var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();
Es kann zwei Ansätze geben:
Verwenden Sie dasselbe Objekt wie folgt erneut:
public Car PaintedIn(Color color) { this.Color = color; return this; }
Erstellen Sie
Car
bei jedem Schritt ein neues Objekt vom Typ :public Car PaintedIn(Color color) { var car = new Car(this); // Clone the current object. car.Color = color; // Assign the values to the clone, not the original object. return car; }
Ist der erste falsch oder eher eine persönliche Entscheidung des Entwicklers?
Ich glaube, dass seine erste Annäherung schnell zu dem intuitiven / irreführenden Code führen kann. Beispiel:
// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);
// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();
// Would `specificModel` car be yellow or of neutral color? How would you guess that if
// `yellowCar` were in a separate method called somewhere else in code?
Irgendwelche Gedanken?
coding-style
readability
method-chaining
Arseni Mourzenko
quelle
quelle
var car = new Car(Brand.Ford, 12345, Color.Silver);
?Antworten:
Ich würde die fließende API einer eigenen "Builder" -Klasse zuweisen, die von dem Objekt, das sie erstellt, getrennt ist. Wenn der Client die flüssige API nicht verwenden möchte, kann sie dennoch manuell verwendet werden, und das Domänenobjekt wird nicht verschmutzt (unter Einhaltung des Grundsatzes der einmaligen Verantwortung). In diesem Fall würde Folgendes erstellt:
Car
Welches ist das Domain-ObjektCarBuilder
welches die flüssige API enthältDie Verwendung wäre wie folgt:
Die
CarBuilder
Klasse würde folgendermaßen aussehen (ich verwende hier die C # -Namenskonvention):Beachten Sie, dass diese Klasse nicht threadsicher ist (jeder Thread benötigt eine eigene CarBuilder-Instanz). Beachten Sie auch, dass eine flüssige API zwar ein wirklich cooles Konzept ist, aber wahrscheinlich zu viel des Guten ist, um einfache Domänenobjekte zu erstellen.
Dieser Deal ist nützlicher, wenn Sie eine API für etwas viel Abstrakteres erstellen und eine komplexere Einrichtung und Ausführung haben. Deshalb eignet er sich hervorragend für Unit-Tests und DI-Frameworks. Weitere Beispiele finden Sie im Java-Abschnitt des Wikipedia Fluent Interface-Artikels mit Informationen zu Persistenz, Datumsangaben und Scheinobjekten.
BEARBEITEN:
Wie aus den Kommentaren hervorgeht; Sie könnten die Builder-Klasse zu einer statischen inneren Klasse machen (innerhalb von Car) und Car könnte unveränderlich gemacht werden. Dieses Beispiel, Auto unveränderlich zu lassen, scheint ein bisschen albern zu sein; Aber in einem komplexeren System, in dem Sie den Inhalt des erstellten Objekts absolut nicht ändern möchten, möchten Sie dies möglicherweise tun.
Im Folgenden finden Sie ein Beispiel für die Ausführung der statischen inneren Klasse und für den Umgang mit der Erstellung eines unveränderlichen Objekts, das erstellt wird:
Die Verwendung wäre die folgende:
Edit 2: Pete hat in den Kommentaren einen Blogbeitrag über die Verwendung von Buildern mit Lambda-Funktionen im Kontext des Schreibens von Komponententests mit komplexen Domänenobjekten verfasst. Es ist eine interessante Alternative, um den Builder etwas ausdrucksvoller zu gestalten.
Wenn
CarBuilder
Sie stattdessen diese Methode benötigen:Welches kann wie folgt verwendet werden:
quelle
build()
(oderBuild()
) aufgerufen , nicht den Namen des Typs, den es erstellt (Car()
in Ihrem Beispiel). WennCar
es sich um ein wirklich unveränderliches Objekt handelt (z. B. alle Felderreadonly
), kann es auch der Builder nicht ändern, sodass dieBuild()
Methode für die Erstellung der neuen Instanz verantwortlich ist. Eine Möglichkeit, dies zu tun, besteht darin,Car
nur einen einzigen Konstruktor zu haben, der einen Builder als Argument verwendet. dann kann dieBuild()
methode ebenreturn new Car(this);
.Das hängt davon ab.
Ist Ihr Auto eine Einheit oder ein Wertobjekt ? Wenn das Auto eine Entität ist, ist die Objektidentität von Bedeutung, daher sollten Sie dieselbe Referenz zurückgeben. Wenn das Objekt ein Wertobjekt ist, sollte es unveränderlich sein. Dies bedeutet, dass jedes Mal eine neue Instanz zurückgegeben werden muss.
Ein Beispiel für Letzteres wäre die DateTime-Klasse in .NET, bei der es sich um ein Wertobjekt handelt.
Wenn das Modell jedoch eine Entität ist, gefällt mir Spoikes Antwort auf die Verwendung einer Builder-Klasse, um Ihr Objekt zu erstellen. Mit anderen Worten, dieses Beispiel, das Sie angegeben haben, ist meiner Meinung nach nur dann sinnvoll, wenn das Auto ein Wertobjekt ist.
quelle
Erstellen Sie einen separaten statischen inneren Builder.
Verwenden Sie normale Konstruktorargumente für die erforderlichen Parameter. Und fließend api für optional.
Erstellen Sie beim Festlegen der Farbe kein neues Objekt, es sei denn, Sie benennen die Methode NewCarInColour oder etwas Ähnliches um.
Ich würde so etwas mit der Marke nach Bedarf und dem Rest optional machen (das ist Java, aber deine sieht aus wie Javascript, ist aber ziemlich sicher, dass sie mit ein bisschen Nit-Picking austauschbar sind):
quelle
Das Wichtigste ist, dass die von Ihnen gewählte Entscheidung im Methodennamen und / oder im Kommentar eindeutig angegeben ist.
Es gibt keinen Standard. Manchmal gibt die Methode ein neues Objekt zurück (die meisten String-Methoden tun dies) oder gibt dieses Objekt aus Gründen der Verkettung oder Speichereffizienz zurück.
Ich habe einmal ein 3D-Vektorobjekt entworfen und für jede mathematische Operation beide Methoden implementiert. Für den Moment die Skalierungsmethode:
quelle
scale
(den Mutator) undscaledBy
(den Generator) nennen.Ich sehe hier ein paar Probleme, die ich für verwirrend halte ... Ihre erste Zeile in der Frage:
Sie rufen einen Konstruktor (neu) und eine create-Methode auf ... Eine create () -Methode ist fast immer eine statische Methode oder eine Buildermethode, und der Compiler sollte sie in einer Warnung oder einem Fehler abfangen, um Sie darüber zu informieren Art und Weise ist diese Syntax entweder falsch oder hat einige schreckliche Namen. Aber später verwenden Sie nicht beide, also schauen wir uns das an.
Wieder mit dem Erstellen, nur nicht mit einem neuen Konstruktor. Ich glaube, Sie suchen stattdessen nach einer copy () -Methode. Also, wenn das der Fall ist und es nur ein schlechter Name ist, sehen wir uns eine Sache an ... Sie nennen mercedes.Paintedin (Color.Yellow) .Copy () - Es sollte einfach sein, das anzuschauen und zu sagen, dass es gemalt wird »Bevor ich kopiert wurde - für mich nur ein normaler logischer Ablauf. Legen Sie also die Kopie an erste Stelle.
Für mich ist es einfach zu sehen, dass Sie die Kopie malen und Ihr gelbes Auto herstellen.
quelle
Der erste Ansatz hat den Nachteil, den Sie erwähnen, aber solange Sie in den Dokumenten klarstellen, dass ein halbkompetenter Programmierer keine Probleme haben sollte. Der gesamte Code für die Verkettung von Methoden, mit dem ich persönlich gearbeitet habe, hat auf diese Weise funktioniert.
Der zweite Ansatz hat offensichtlich den Nachteil, mehr Arbeit zu haben. Sie müssen sich auch entscheiden, ob die von Ihnen zurückgegebenen Kopien flache oder tiefe Kopien enthalten sollen. Dies kann von Klasse zu Klasse oder von Methode zu Methode variieren, sodass Sie entweder Inkonsistenzen einführen oder das beste Verhalten beeinträchtigen. Es ist erwähnenswert, dass dies die einzige Option für unveränderliche Objekte wie Zeichenfolgen ist.
Was auch immer Sie tun, mischen Sie nicht in derselben Klasse!
quelle
Ich würde eher wie der Mechanismus "Erweiterungsmethoden" denken.
quelle
Dies ist eine Variation der obigen Methoden. Der Unterschied besteht darin, dass es statische Methoden in der Car-Klasse gibt, die mit den Methodennamen im Builder übereinstimmen, sodass Sie keinen Builder explizit erstellen müssen:
Sie können dieselben Methodennamen verwenden, die Sie für die verketteten Builder-Aufrufe verwenden:
Außerdem gibt es eine .copy () -Methode für die Klasse, die einen Builder zurückgibt, der mit allen Werten der aktuellen Instanz gefüllt ist, sodass Sie eine Variation eines Themas erstellen können:
Schließlich prüft die .build () -Methode des Builders, ob alle erforderlichen Werte angegeben wurden, und gibt gegebenenfalls einen Auslöser aus. Es ist möglicherweise vorzuziehen, einige Werte für den Konstruktor des Builders anzufordern und den Rest optional zu lassen. In diesem Fall möchten Sie eines der Muster in den anderen Antworten.
quelle