Quando si utilizza il metodo di concatenazione, riutilizzare l'oggetto o crearne uno?

37

Quando usi il metodo di concatenamento come:

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

potrebbero esserci due approcci:

  • Riutilizza lo stesso oggetto, in questo modo:

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
    
  • Crea un nuovo oggetto di tipo Car ad ogni passo, in questo modo:

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

Il primo sbagliato o è piuttosto una scelta personale dello sviluppatore?

Credo che il suo primo approccio possa rapidamente causare il codice intuitivo / fuorviante. Esempio:

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

Qualche idea?

    
posta Arseni Mourzenko 25.05.2012 - 10:54
fonte

8 risposte

41

Metteremo la fluent api alla sua classe "builder" separata dall'oggetto che sta creando. In questo modo, se il cliente non desidera utilizzare l'API fluente, è comunque possibile utilizzarlo manualmente e non inquina l'oggetto dominio (attenendosi al principio di responsabilità singola). In questo caso verrà creato il seguente:

  • Car che è l'oggetto dominio
  • CarBuilder che contiene l'API fluente

L'utilizzo sarebbe simile a questo:

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

La classe CarBuilder sarà simile a questa (sto usando la convenzione di denominazione C # qui):

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

}

Si noti che questa classe non sarà thread-safe (ogni thread avrà bisogno della propria istanza di CarBuilder). Nota anche che, anche se l'api fluente è un concetto davvero interessante, probabilmente è eccessivo per lo scopo di creare oggetti di dominio semplici.

Questo accordo è più utile se stai creando un'API per qualcosa di molto più astratto e ha un set up e un'esecuzione più complessi, ed è per questo che funziona alla grande nelle prove unitarie e nei quadri DI. Puoi vedere alcuni altri esempi nella sezione Java di wikipedia Fluent Interface article con persistenza, gestione delle date e oggetti mock .

EDIT:

Come notato dai commenti; potresti rendere la classe Builder una classe interna statica (all'interno di Car) e Car potrebbe essere resa immutabile. Questo esempio di lasciare che l'auto sia immutabile sembra un po 'sciocco; ma in un sistema più complesso, in cui non si desidera assolutamente modificare il contenuto dell'oggetto che è stato creato, si potrebbe desiderare di farlo.

Di seguito è riportato un esempio di come eseguire sia la classe interna statica che come gestire una creazione di un oggetto immutabile che genera:

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

L'utilizzo sarebbe il seguente:

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

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

Modifica 2: Pete nei commenti ha fatto un post di blog sull'utilizzo di builder con funzioni lambda nel contesto della scrittura di unit test con oggetti di dominio complessi. È un'alternativa interessante per rendere il costruttore un po 'più espressivo.

Nel caso di CarBuilder devi invece avere questo metodo:

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

Che può essere usato come questo:

Car c = CarBuilder
    .Build(car => 
        car.OfBrand(Brand.Ford)
           .OfModel(12345)
           .PaintedIn(Color.Silver);
    
risposta data 25.05.2012 - 11:16
fonte
9

Dipende.

La tua auto è una entità o un Oggetto valore ? Se l'auto è un'entità, l'identità dell'oggetto è importante, quindi è necessario restituire lo stesso riferimento. Se l'oggetto è un oggetto valore, dovrebbe essere immutabile, ovvero l'unico modo è quello di restituire ogni volta una nuova istanza.

Un esempio di quest'ultimo sarebbe la classe DateTime in .NET, che è un oggetto valore.

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.

Tuttavia, se il modello è un'entità, mi piace la risposta di Spoike sull'utilizzo di una classe di builder per creare l'oggetto. In altre parole, l'esempio che hai dato ha senso solo se la Car è un oggetto valore.

    
risposta data 25.05.2012 - 13:33
fonte
6

Crea un generatore interno statico separato.

Utilizzare gli argomenti normali del costruttore per i parametri richiesti. E API fluente per facoltativo.

Non creare un nuovo oggetto quando si imposta il colore, a meno che non si rinomino il metodo NewCarInColour o qualcosa di simile.

Vorrei fare qualcosa del genere con il marchio richiesto e il resto facoltativo (questo è java, ma il tuo sembra come javascript, ma piuttosto sicuro che siano intercambiabili con un po 'di nit picking):

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

Car specificYellowModel =new Car.Builder(Brand.MercedesBenz).WithModel(99).PaintedIn(Color.Yellow).create();
    
risposta data 25.05.2012 - 11:31
fonte
4

La cosa più importante è che qualunque sia la decisione che scegli, è chiaramente indicata nel nome del metodo e / o nel commento.

Non esiste uno standard, a volte il metodo restituirà un nuovo oggetto (la maggior parte dei metodi String lo fa) o restituirà questo oggetto per scopi concatenati o per l'efficienza della memoria).

Una volta ho progettato un oggetto 3D vettoriale e per ogni operazione matematica ho implementato entrambi i metodi. Per istante il metodo di scala:

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);
}
    
risposta data 25.05.2012 - 11:18
fonte
3

Qui vedo alcuni problemi che a mio avviso potrebbero creare confusione ... La prima riga nella domanda:

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

Stai chiamando un costruttore (nuovo) e un metodo di creazione ... Un metodo create () sarebbe quasi sempre un metodo statico o un metodo builder, e il compilatore dovrebbe prenderlo in un avvertimento o errore per permetterti di sappi, in entrambi i casi, questa sintassi è sbagliata o ha dei nomi terribili. Ma in seguito, non li usi entrambi, quindi diamo un'occhiata a questo.

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

Ancora una volta con il creare, ma non con un nuovo costruttore. Il fatto è che, invece, stai cercando un metodo copy (). Quindi, se è così, ed è solo un nome povero, guardiamo una cosa ... tu chiami mercedes.Paintedin (Color.Yellow) .Copy () - Dovrebbe essere facile da guardare e dire che è stato dipinto 'prima di essere copiato - solo un normale flusso di logica, per me. Quindi metti prima la copia.

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

per me, è facile vedere lì che stai dipingendo la copia, rendendo la tua auto gialla.

    
risposta data 25.05.2012 - 17:56
fonte
1

Il primo approccio ha lo svantaggio di cui parli, ma finché si chiarisce nei documenti qualsiasi codificatore semi-competente non dovrebbe avere problemi. Tutto il codice di concatenamento dei metodi con cui ho lavorato personalmente ha funzionato in questo modo.

Il secondo approccio ha ovviamente lo svantaggio di essere più lavoro. Devi anche decidere se le copie che restituirai eseguiranno copie poco profonde o profonde: la cosa migliore potrebbe variare da classe a classe o metodo a metodo, quindi dovrai introdurre incoerenza o compromettere il comportamento migliore. Vale la pena notare che questa è l'unica opzione per oggetti immutabili, come le stringhe.

Qualunque cosa tu faccia, non mescolare e abbinare all'interno della stessa classe!

    
risposta data 25.05.2012 - 11:18
fonte
1

Preferisco pensare come il meccanismo dei "Metodi di estensione".

public Car PaintedIn(this Car car, Color color)
{
    car.Color = color;
    return car;
}
    
risposta data 25.05.2012 - 23:24
fonte
0

Questa è una variazione dei metodi precedenti. Le differenze sono che ci sono metodi statici sulla classe Car che corrispondono ai nomi dei metodi sul Builder, quindi non è necessario creare un Builder in modo esplicito:

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

È possibile utilizzare gli stessi nomi di metodo che si usano nelle chiamate del costruttore concatenato:

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

Inoltre, c'è un metodo .copy () sulla classe che restituisce un builder popolato con tutti i valori dall'istanza corrente, quindi puoi creare una variazione su un tema:

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

Infine, il metodo .build () del builder controlla che tutti i valori richiesti siano stati forniti e se ce ne sono mancanti. Potrebbe essere preferibile richiedere alcuni valori sul costruttore del builder e consentire al resto di essere facoltativo; in tal caso, vorrai uno dei pattern nelle altre risposte.

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);
        }
    }
}
    
risposta data 01.06.2012 - 03:44
fonte

Leggi altre domande sui tag