Czy podczas używania metody łączenia łańcuchowego ponownie wykorzystuję obiekt, czy go utworzę?

37

Podczas korzystania z metody łączenia, takiej jak:

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

mogą być dwa podejścia:

  • Ponownie użyj tego samego obiektu, jak poniżej:

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
  • Utwórz nowy obiekt typu Carna każdym kroku, na przykład:

    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;
    }

Czy pierwszy jest zły, czy raczej osobisty wybór dewelopera?


Wierzę, że pierwsze podejście może szybko spowodować intuicyjny / wprowadzający w błąd kod. Przykład:

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

jakieś pomysły?

Arseni Mourzenko
źródło
1
Co jest nie tak z var car = new Car(Brand.Ford, 12345, Color.Silver);?
James
12
@James konstruktor teleskopowy, płynny wzorzec może pomóc odróżnić parametry opcjonalne od wymaganych (jeśli są to wymagane argumenty konstruktora, jeśli nie są opcjonalne). A biegły jest raczej przyjemny do czytania.
NimChimpsky
8
@NimChimpsky, co się stało z dobrymi staroświeckimi (dla C #) właściwościami i konstruktorem, który ma pola, które są wymagane - nie dlatego, że robię płynne interfejsy API, jestem wielkim fanem, ale często są one nadużywane
Chris S
8
@ChrisS, jeśli polegasz na seterach (jestem z javy), musisz sprawić, by twoje obiekty były zmienne, czego możesz nie chcieć robić. A także płynniejsze korzystanie z satelity - wymaga mniej myślenia, ide prawie konstruuje Twój obiekt dla Ciebie.
NimChimpsky
1
@NimChimpsky yeh Widzę, jak płynny jest duży krok naprzód dla Javy
Chris S

Odpowiedzi:

41

Umieściłem płynny interfejs API we własnej klasie „builder” oddzielonej od obiektu, który tworzy. W ten sposób, jeśli klient nie chce korzystać z płynnego interfejsu API, nadal można go używać ręcznie i nie zanieczyszcza on obiektu domeny (zgodnie z zasadą pojedynczej odpowiedzialności). W takim przypadku zostaną utworzone:

  • Car który jest obiektem domeny
  • CarBuilder który posiada płynne API

Użycie byłoby takie:

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

CarBuilderKlasa będzie wyglądać następująco (używam C # konwencję nazewnictwa tutaj):

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;
    }

}

Zauważ, że ta klasa nie będzie bezpieczna dla wątków (każdy wątek będzie potrzebował własnej instancji CarBuilder). Zauważ też, że chociaż płynne API jest naprawdę fajną koncepcją, prawdopodobnie jest to przesada w celu tworzenia prostych obiektów domenowych.

Ta oferta jest bardziej przydatna, jeśli tworzysz interfejs API dla czegoś znacznie bardziej abstrakcyjnego i ma bardziej skomplikowane konfigurowanie i wykonywanie, dlatego świetnie sprawdza się w testowaniu jednostkowym i frameworkach DI. Możesz zobaczyć inne przykłady w sekcji Java artykułu w Wikipedii Płynny interfejs z trwałością, obsługą dat i próbnymi obiektami.


EDYTOWAĆ:

Jak zauważono w komentarzach; można uczynić klasę Konstruktora statyczną klasą wewnętrzną (wewnątrz Samochodu), a Samochód można uczynić niezmiennym. Ten przykład pozwalający Carowi pozostać niezmiennym wydaje się trochę głupi; ale w bardziej złożonym systemie, w którym absolutnie nie chcesz zmieniać zawartości budowanego obiektu, możesz to zrobić.

Poniżej znajduje się przykład tego, jak wykonać zarówno statyczną klasę wewnętrzną, jak i jak obsługiwać niezmienne stworzenie obiektu, które buduje:

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

Wykorzystanie byłoby następujące:

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

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

Edycja 2: Pete w komentarzach napisał post na blogu o używaniu konstruktorów z funkcjami lambda w kontekście pisania testów jednostkowych złożonych obiektów domeny. To ciekawa alternatywa, aby budowniczy był bardziej wyrazisty.

W takim przypadku CarBuildermusisz zamiast tego skorzystać z tej metody:

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

Które mogą być użyte jako to:

Car c = CarBuilder
    .Build(car => 
        car.OfBrand(Brand.Ford)
           .OfModel(12345)
           .PaintedIn(Color.Silver);
Łup
źródło
3
@Baqueta to jest opisana skuteczna java Josha Blocha
NimChimpsky
6
@Baqueta wymaga czytania dla java dev, imho.
NimChimpsky
3
Ogromną zaletą IMHO jest to, że możesz użyć tego wzorca (jeśli odpowiednio zmodyfikowany), aby zapobiec ucieczce budowniczego przez obiekty w budowie, które nie są ukończone. Np. Możesz upewnić się, że nie będzie samochodu o nieokreślonym kolorze.
scarfridge
1
Hmm ... Zawsze nazywałem ostatnią metodę wzorca konstruktora build()(lub Build()), a nie nazwę typu, który buduje ( Car()w twoim przykładzie). Ponadto, jeśli Carjest to naprawdę niezmienny obiekt (np. Wszystkie jego pola są readonly), to nawet konstruktor nie będzie w stanie go zmutować, więc Build()metoda staje się odpowiedzialna za budowę nowej instancji. Jednym ze sposobów na to jest Carposiadanie tylko jednego konstruktora, który przyjmuje jako argument Konstruktor; wtedy Build()metoda może return new Car(this);.
Daniel Pryden
1
Napisałem blog o innym podejściu do tworzenia konstruktorów opartych na lambdach. Post prawdopodobnie wymaga trochę edycji. Mój kontekst dotyczył głównie zakresu testu jednostkowego, ale w stosownych przypadkach można go również zastosować w innych obszarach. Można go znaleźć tutaj: petesdotnet.blogspot.com/2012/05/…
Pete
9

To zależy.

Czy Twój samochód jest podmiotem czy obiektem wartości ? Jeśli samochód jest bytem, ​​ważna jest tożsamość obiektu, dlatego powinieneś zwrócić to samo odniesienie. Jeśli obiekt jest obiektem wartości, powinien być niezmienny, co oznacza, że ​​jedynym sposobem jest zwracanie nowej instancji za każdym razem.

Przykładem tego ostatniego może być klasa DateTime w .NET, która jest obiektem wartości.

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.

Jeśli jednak model jest bytem, ​​podoba mi się odpowiedź Spoike dotycząca użycia klasy konstruktora do zbudowania obiektu. Innymi słowy, podany przez ciebie przykład ma sens tylko wtedy, gdy samochód jest obiektem wartości.

Pete
źródło
1
+1 za pytanie „Podmiot” a „Wartość”. To pytanie, czy twoja klasa jest zmiennym, czy niezmiennym typem (czy ten obiekt powinien zostać zmieniony?) I zależy wyłącznie od ciebie, choć wpłynie to na twój projekt. Zwykle nie spodziewałbym się, że łączenie metod będzie działało na typie zmiennym, chyba że metoda zwróci nowy obiekt.
Casey Kuball,
6

Utwórz osobny statyczny wewnętrzny konstruktor.

Użyj normalnych argumentów konstruktora dla wymaganych parametrów. I płynne API dla opcjonalnego.

Nie twórz nowego obiektu podczas ustawiania koloru, chyba że zmienisz nazwę metody NewCarInColour lub coś podobnego.

Zrobiłbym coś takiego z marką zgodnie z wymaganiami, a reszta opcjonalna (to java, ale twój wygląda jak javascript, ale całkiem pewne, że można je zamieniać z odrobiną nitowania):

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

Car specificYellowModel =new Car.Builder(Brand.MercedesBenz).WithModel(99).PaintedIn(Color.Yellow).create();
Nim Chimpsky
źródło
4

Najważniejsze jest to, że niezależnie od wybranej decyzji, jest to wyraźnie określone w nazwie metody i / lub komentarzu.

Nie ma standardu, czasami metoda zwróci nowy obiekt (większość metod String to robi) lub zwróci ten obiekt w celu utworzenia łańcucha lub wydajności pamięci).

Kiedyś zaprojektowałem obiekt Vector Vector i dla każdej operacji matematycznej wdrożyłem obie metody. Dla natychmiastowej metody skalowania:

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
źródło
3
+1. Bardzo dobra uwaga. Naprawdę nie rozumiem, dlaczego to miało negatywne zdanie. Zwrócę jednak uwagę, że nazwy, które wybraliście, nie są bardzo jasne. Nazwałbym ich scale(mutatorem) i scaledBy(generatorem).
back2dos
Dobrze, że nazwy mogłyby być wyraźniejsze. Nazewnictwo odbywało się zgodnie z konwencją innych klas matematycznych, z których korzystałem z biblioteki. Efekt został również podany w komentarzach javadoc do metody, aby uniknąć nieporozumień.
XGouchet,
3

Widzę tutaj kilka problemów, które mogą być mylące ... Twoja pierwsza linia w pytaniu:

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

Wywołujesz konstruktor (nowy) i metodę tworzenia ... Metoda create () prawie zawsze byłaby metodą statyczną lub metodą konstruktora, a kompilator powinien złapać ją w ostrzeżeniu lub błędzie, aby poinformować Cię, albo sposób ta składnia jest nieprawidłowa lub ma jakieś okropne nazwy. Ale później nie używasz obu, więc spójrzmy na to.

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

Znowu z tworzeniem, ale nie z nowym konstruktorem. Rzecz w tym, że myślę, że zamiast tego szukasz metody copy (). Więc jeśli tak jest, a to tylko kiepskie imię, spójrzmy na jedną rzecz ... nazywasz mercedes.Paintedin (Color.Yellow) .Copy () - Powinno być łatwo na to spojrzeć i powiedzieć, że jest malowany „przed skopiowaniem - dla mnie zwykły przepływ logiki. Więc umieść kopię na pierwszym miejscu.

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

dla mnie łatwo jest zauważyć, że malujesz kopię, robiąc swój żółty samochód.

Drake Clarris
źródło
+1 za wskazanie dysonansu między nowym a Create ();
Joshua Drake
1

Pierwsze podejście ma tę wadę, o której wspomniałeś, ale tak długo, jak wyjaśnisz to w dokumentach, żaden na wpół kompetentny programista nie powinien mieć problemów. Cały kod łańcucha, z którym osobiście pracowałem, działał w ten sposób.

Drugie podejście ma oczywiście tę wadę, że wymaga więcej pracy. Musisz także zdecydować, czy kopie, które zwrócisz, będą wykonywać kopie płytkie, czy głębokie: co jest najlepsze, może różnić się w zależności od klasy lub metody, tak więc albo wprowadzisz niekonsekwencję, albo kompromis w sprawie najlepszego zachowania. Warto zauważyć, że jest to jedyna opcja dla niezmiennych obiektów, takich jak łańcuchy.

Cokolwiek robisz, nie mieszaj i nie dopasowuj w tej samej klasie!

Vaughandroid
źródło
1

Wolę myśleć tak jak mechanizm „Metody rozszerzenia”.

public Car PaintedIn(this Car car, Color color)
{
    car.Color = color;
    return car;
}
Amir Karimi
źródło
0

Jest to odmiana powyższych metod. Różnice polegają na tym, że istnieją klasy statyczne w klasie Car, które pasują do nazw metod w Konstruktorze, więc nie trzeba jawnie tworzyć Konstruktora:

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

Możesz użyć tych samych nazw metod, których używasz w połączonych wywołaniach konstruktora:

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

Ponadto w klasie znajduje się metoda .copy (), która zwraca konstruktora wypełnionego wszystkimi wartościami z bieżącej instancji, dzięki czemu można utworzyć wariację na temat:

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

Na koniec metoda .build () konstruktora sprawdza, czy wszystkie wymagane wartości zostały podane, i zgłasza, jeśli brakuje. Może być pożądane, aby wymagać pewnych wartości w konstruktorze konstruktora i pozwolić, aby reszta była opcjonalna; w takim przypadku chcesz jeden z wzorców w pozostałych odpowiedziach.

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
źródło