Verwende ich das Objekt beim Verwenden der Methodenverkettung erneut oder erstelle ich ein Objekt?

37

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 Carbei 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?

Arseni Mourzenko
quelle
1
Was ist los mit var car = new Car(Brand.Ford, 12345, Color.Silver);?
James
12
@James Teleskopkonstruktor, das fließende Muster kann dabei helfen, zwischen optionalen und erforderlichen Parametern zu unterscheiden (wenn Konstruktorargumente erforderlich sind, wenn nicht optional). Und das Fließende ist ziemlich schön zu lesen.
NimChimpsky
8
@NimChimpsky, was mit guten, altmodischen (für C #) Eigenschaften passiert ist, und einem Konstruktor mit den erforderlichen Feldern - nicht, dass ich Fluent APIs blaste, ich bin ein großer Fan, aber sie werden oft überbeansprucht
Chris S
8
@ChrisS Wenn Sie sich auf Setter verlassen (ich bin von Java), müssen Sie Ihre Objekte veränderbar machen, was Sie vielleicht nicht möchten. Und wenn Sie fließend sprechen, erhalten Sie auch eine schönere intellitext - dies erfordert weniger Nachdenken, die ide konstruiert fast Ihr Objekt für Sie.
NimChimpsky
1
@NimChimpsky yeh Ich kann sehen, wie fließend ein großer Sprung nach vorne für Java ist
Chris S

Antworten:

41

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-Objekt
  • CarBuilder welches die flüssige API enthält

Die Verwendung wäre wie folgt:

var car = CarBuilder.BuildCar()
    .OfBrand(Brand.Ford)
    .OfModel(12345)
    .PaintedIn(Color.Silver)
    .Build();

Die CarBuilderKlasse würde folgendermaßen aussehen (ich verwende hier die C # -Namenskonvention):

public class CarBuilder {

    private Car _car;

    /// Constructor
    public CarBuilder() {
        _car = new Car();
        SetDefaults();
    }

    private void SetDefaults() {
        this.OfBrand(Brand.Ford);
          // you can continue the chaining for 
          // other default values
    }

    /// Starts an instance of the car builder to 
    /// build a new car with default values.
    public static CarBuilder BuildCar() {
        return new CarBuilder();
    }

    /// Sets the brand
    public CarBuilder OfBrand(Brand brand) {
        _car.SetBrand(brand);
        return this;
    }

    // continue with OfModel(...), PaintedIn(...), and so on...
    // that returns "this" to allow method chaining

    /// Returns the built car
    public Car Build() {
        return _car;
    }

}

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:

// the class that represents the immutable object
public class ImmutableWriter {

    // immutable variables
    private int _times; private string _write;

    // the "complex" constructor
    public ImmutableWriter(int times, string write) {
        _times = times;
        _write = write;
    }

    public void Perform() {
        for (int i = 0; i < _times; i++) Console.Write(_write + " ");
    }

    // static inner builder of the immutable object
    protected static class ImmutableWriterBuilder {

        // the variables needed to construct the immutable object
        private int _ii = 0; private string _is = String.Empty;

        public void Times(int i) { _ii = i; }

        public void Write(string s) { _is = s; }

        // The stuff is all built here
        public ImmutableWriter Build() {
            return new ImmutableWriter(_ii, _is);
        }

    }

    // factory method to get the builder
    public static ImmutableWriterBuilder GetBuilder() {
        return new ImmutableWriterBuilder();
    }
}

Die Verwendung wäre die folgende:

var writer = ImmutableWriter
                .GetBuilder()
                .Write("peanut butter jelly time")
                .Times(2)
                .Build();

writer.Perform();
// console writes: peanut butter jelly time peanut butter jelly time 

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 CarBuilderSie stattdessen diese Methode benötigen:

public static Car Build(Action<CarBuilder> buildAction = null) {
    var carBuilder = new CarBuilder();
    if (buildAction != null) buildAction(carBuilder);
    return carBuilder._car;
}

Welches kann wie folgt verwendet werden:

Car c = CarBuilder
    .Build(car => 
        car.OfBrand(Brand.Ford)
           .OfModel(12345)
           .PaintedIn(Color.Silver);
Spoike
quelle
3
@ Baqueta Dies ist Josh Blochs effektive Java skizziert
NimChimpsky
6
@ Baqueta benötigt Lesung für Java Dev, imho.
NimChimpsky
3
IMHO ist ein großer Vorteil, dass Sie dieses Muster verwenden können (wenn es entsprechend geändert wird), um zu verhindern, dass Instanzen des im Bau befindlichen Objekts, die nicht abgeschlossen sind, dem Builder entkommen. ZB können Sie sicherstellen, dass es kein Auto mit einer undefinierten Farbe gibt.
scarfridge
1
Hmm ... Ich habe immer die endgültige Methode des Builder-Musters build()(oder Build()) aufgerufen , nicht den Namen des Typs, den es erstellt ( Car()in Ihrem Beispiel). Wenn Cares sich um ein wirklich unveränderliches Objekt handelt (z. B. alle Felder readonly), kann es auch der Builder nicht ändern, sodass die Build()Methode für die Erstellung der neuen Instanz verantwortlich ist. Eine Möglichkeit, dies zu tun, besteht darin, Carnur einen einzigen Konstruktor zu haben, der einen Builder als Argument verwendet. dann kann die Build()methode eben return new Car(this);.
Daniel Pryden
1
Ich habe über einen anderen Ansatz gebloggt, um Builder basierend auf Lambdas zu erstellen. Der Beitrag muss wahrscheinlich noch bearbeitet werden. Mein Kontext war größtenteils der im Rahmen eines Komponententests, er konnte jedoch gegebenenfalls auch auf andere Bereiche angewendet werden. Es kann hier gefunden werden: petesdotnet.blogspot.com/2012/05/…
Pete
9

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.

var date1 = new DateTime(2012,1,1);
var date2 = date1.AddDays(1);
// date2 now refers to Jan 2., while date1 remains unchanged at Jan 1.

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.

Pete
quelle
1
+1 für die Frage 'Entität' gegen 'Wert'. Es ist eine Frage, ob es sich bei Ihrer Klasse um einen veränderlichen oder einen unveränderlichen Typ handelt (sollte dieses Objekt geändert werden?), Und es liegt ganz bei Ihnen, auch wenn dies Auswirkungen auf Ihr Design hat. Normalerweise würde ich nicht erwarten, dass die Methodenverkettung für einen veränderlichen Typ funktioniert, es sei denn, die Methode gibt ein neues Objekt zurück.
Casey Kuball
6

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):

Car yellowMercedes = new Car.Builder(Brand.MercedesBenz).PaintedIn(Color.Yellow).create();

Car specificYellowModel =new Car.Builder(Brand.MercedesBenz).WithModel(99).PaintedIn(Color.Yellow).create();
NimChimpsky
quelle
4

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:

Vector3D scaleLocal(float factor){
    this.x *= factor; 
    this.y *= factor; 
    this.z *= factor; 
    return this;
}

Vector3D scale(float factor){
    Vector3D that = new Vector3D(this); // clone this vector
    return that.scaleLocal(factor);
}
XGouchet
quelle
3
+1. Sehr guter Punkt. Ich verstehe nicht wirklich, warum dies eine Ablehnung bekam. Ich werde jedoch bemerken, dass die von Ihnen gewählten Namen nicht sehr klar sind. Ich würde sie scale(den Mutator) und scaledBy(den Generator) nennen.
back2dos
Guter Punkt, Namen hätten klarer sein können. Die Benennung folgte einer Konvention anderer mathematischer Klassen, die ich aus einer Bibliothek verwendete. Der Effekt wurde auch in den Javadoc-Kommentaren der Methode angegeben, um Verwirrung zu vermeiden.
XGouchet
3

Ich sehe hier ein paar Probleme, die ich für verwirrend halte ... Ihre erste Zeile in der Frage:

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

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.

// 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();

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.

var yellowCar = mercedes.Copy().PaintedIn(Color.Yellow)

Für mich ist es einfach zu sehen, dass Sie die Kopie malen und Ihr gelbes Auto herstellen.

Drake Clarris
quelle
+1, um auf die Dissonanz zwischen new und Create () hinzuweisen;
Joshua Drake
1

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!

vaughandroid
quelle
1

Ich würde eher wie der Mechanismus "Erweiterungsmethoden" denken.

public Car PaintedIn(this Car car, Color color)
{
    car.Color = color;
    return car;
}
Amir Karimi
quelle
0

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:

Car car = Car.builder().ofBrand(Brand.Ford).ofColor("Green")...

Sie können dieselben Methodennamen verwenden, die Sie für die verketteten Builder-Aufrufe verwenden:

Car car = Car.ofBrand(Brand.Ford).ofColor("Green")...

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:

Car red = car.copy().paintedIn("Red").build();

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.

public enum Brand {
    Ford, Chrysler, GM, Honda, Toyota, Mercedes, BMW, Lexis, Tesla;
}

public class Car {
    private final Brand brand;
    private final int model;
    private final String color;

    public Car(Brand brand, int model, String color) {
        this.brand = brand;
        this.model = model;
        this.color = color;
    }

    public Brand getBrand() {
        return brand;
    }

    public int getModel() {
        return model;
    }

    public String getColor() {
        return color;
    }

    @Override public String toString() {
        return brand + " " + model + " " + color;
    }

    public Builder copy() {
        Builder builder = new Builder();
        builder.brand = brand;
        builder.model = model;
        builder.color = color;
        return builder;
    }

    public static Builder ofBrand(Brand brand) {
        Builder builder = new Builder();
        builder.brand = brand;
        return builder;
    }

    public static Builder ofModel(int model) {
        Builder builder = new Builder();
        builder.model = model;
        return builder;
    }

    public static Builder paintedIn(String color) {
        Builder builder = new Builder();
        builder.color = color;
        return builder;
    }

    public static class Builder {
        private Brand brand = null;
        private Integer model = null;
        private String color = null;

        public Builder ofBrand(Brand brand) {
            this.brand = brand;
            return this;
        }

        public Builder ofModel(int model) {
            this.model = model;
            return this;
        }

        public Builder paintedIn(String color) {
            this.color = color;
            return this;
        }

        public Car build() {
            if (brand == null) throw new IllegalArgumentException("no brand");
            if (model == null) throw new IllegalArgumentException("no model");
            if (color == null) throw new IllegalArgumentException("no color");
            return new Car(brand, model, color);
        }
    }
}
David Conrad
quelle