Quando si utilizza il metodo concatenamento, posso riutilizzare l'oggetto o crearne uno?


37

Quando si utilizza il metodo concatenamento come:

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

ci possono essere due approcci:

  • Riutilizzare lo stesso oggetto, in questo modo:

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
  • Crea un nuovo oggetto di tipo Carad 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 causare rapidamente 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?


1
Cosa c'è che non va var car = new Car(Brand.Ford, 12345, Color.Silver);?
James,

12
Costruttore telescopico @James, il modello fluente può aiutare a distinguere tra parametri opzionali e parametri obbligatori (se sono costruttori, è necessario, se non facoltativo). E il fluente è piuttosto bello da leggere.
NimChimpsky,

8
@NimChimpsky cosa è successo alle buone proprietà vecchio stile (per C #) e un costruttore che ha i campi richiesti - non che sto distruggendo API fluenti, sono un grande fan ma sono spesso abusati
Chris S

8
@ChrisS se ti affidi ai setter (io sono di Java) devi rendere i tuoi oggetti mutabili, cosa che potresti non voler fare. E ottieni anche un intellitext migliore quando usi fluentemente - richiede meno pensiero, l'ide costruisce quasi il tuo oggetto per te.
NimChimpsky,

1
@NimChimpsky yeh Vedo che è fluente fare un grande balzo in avanti per Java
Chris S,

Risposte:


41

Metterei l' API fluente nella propria classe "costruttore" separata dall'oggetto che sta creando. In questo modo, se il client non desidera utilizzare l'API fluente, è comunque possibile utilizzarlo manualmente e non inquina l'oggetto di dominio (aderendo al principio di responsabilità singola). In questo caso verrebbe creato:

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

L'utilizzo sarebbe così:

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

La CarBuilderclasse sarebbe 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;
    }

}

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

Questo accordo è più utile se stai creando un'API per qualcosa di molto più astratto e ha un'impostazione e un'esecuzione più complesse, motivo per cui funziona alla grande in unit test e framework DI. Puoi vedere alcuni altri esempi nella sezione Java dell'articolo di Wikipedia Fluent Interface con persistenza, gestione della data e oggetti finti.


MODIFICARE:

Come notato dai commenti; potresti rendere la classe Builder una classe interna statica (all'interno di Car) e Car rendere immutabile. Questo esempio di lasciare immutabile Car sembra un po 'sciocco; ma in un sistema più complesso, in cui non vuoi assolutamente cambiare il contenuto dell'oggetto che è stato creato, potresti volerlo fare.

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

// 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 pubblicato un post sul blog sull'uso dei costruttori 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 CarBuildertu abbia invece bisogno di questo metodo:

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

Quale può essere usato come questo:

Car c = CarBuilder
    .Build(car => 
        car.OfBrand(Brand.Ford)
           .OfModel(12345)
           .PaintedIn(Color.Silver);

3
@Baqueta questo è delineato java
bloch

6
@Baqueta ha richiesto la lettura di java dev, imho.
NimChimpsky,

3
IMHO un enorme vantaggio è che puoi usare questo modello (se modificato in modo appropriato) per impedire alle istanze dell'oggetto in costruzione che non sono state completate di sfuggire al builder. Ad esempio, puoi assicurarti che non ci sarà un'auto con un colore indefinito.
scarfridge,

1
Hmm ... Ho sempre chiamato il metodo finale del pattern builder build()(o Build()), non il nome del tipo che costruisce ( Car()nel tuo esempio). Inoltre, se Carè un oggetto veramente immutabile (ad esempio, tutti i suoi campi lo sono readonly), anche il builder non sarà in grado di mutarlo, quindi il Build()metodo diventa responsabile della costruzione della nuova istanza. Un modo per farlo è avere Carun solo costruttore, che prende come argomento un Builder; allora il Build()metodo può semplicemente return new Car(this);.
Daniel Pryden,

1
Ho fatto un blog su un diverso approccio alla creazione di costruttori basato su lambdas. Il post probabilmente ha bisogno di un po 'di modifica. Il mio contesto era principalmente quello dell'ambito di un test unitario, ma poteva essere applicato anche ad altre aree, se applicabile. Può essere trovato qui: petesdotnet.blogspot.com/2012/05/…
Pete

9

Dipende.

La tua auto è un'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, il che significa che l'unico modo è restituire una nuova istanza ogni volta.

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 builder per costruire l'oggetto. In altre parole, quell'esempio che hai dato ha senso solo se l'auto è un oggetto valore.


1
+1 per la domanda "Entità" vs "Valore". È una questione se la tua classe è un tipo mutabile o immutabile (questo oggetto dovrebbe essere cambiato?) E dipende completamente da te, anche se influenzerà il tuo design. In genere non mi aspetto che il concatenamento di metodi funzioni su un tipo mutabile, a meno che il metodo non abbia restituito un nuovo oggetto.
Casey Kuball,

6

Crea un builder interno statico separato.

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

Non creare un nuovo oggetto durante l'impostazione del colore, a meno che non si rinomina il metodo NewCarInColour o qualcosa di simile.

Farei una cosa del genere con il marchio come richiesto e il resto facoltativo (questo è Java, ma il tuo sembra javascript, ma abbastanza 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();

4

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

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

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

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

3
+1. Ottimo punto Non vedo davvero perché questo abbia un downvote. Noterò comunque che i nomi che hai scelto non sono molto chiari. Li chiamerei scale(il mutatore) e scaledBy(il generatore).
back2dos,

Bene, i nomi avrebbero potuto essere più chiari. La denominazione ha seguito una convenzione di altre classi matematiche che ho usato da una biblioteca. L'effetto è stato anche affermato nei commenti javadoc del metodo per evitare confusione.
XGouchet,

3

Vedo alcuni problemi qui che penso possano essere fonte di confusione ... La tua 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 create ... Un metodo create () sarebbe quasi sempre un metodo statico o un metodo builder, e il compilatore dovrebbe prenderlo in un avviso o errore per farti sapere, sia modo, questa sintassi è sbagliata o ha alcuni nomi terribili. Ma più tardi, non li usi entrambi, quindi diamo un'occhiata a quello.

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

Anche in questo caso con la creazione, ma non con un nuovo costruttore. Il fatto è che penso che tu stia cercando un metodo copy () invece. Quindi, se è così, ed è solo un povero nome, diamo un'occhiata a una cosa ... chiami mercedes.Paintedin (Color.Yellow) .Copy () - Dovrebbe essere facile guardarlo e dire che è '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 macchina gialla.


+1 per indicare la dissonanza tra il nuovo e Create ();
Joshua Drake,

1

Il primo approccio ha lo svantaggio di cui parli, ma fintanto che lo chiarisci nei documenti qualsiasi programmatore semi-competente non dovrebbe avere problemi. Tutto il codice di concatenamento di 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 restituite saranno copie superficiali o profonde: la cosa migliore può variare da classe a classe o da metodo a metodo, quindi introdurrai incoerenze o comprometterai 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!


1

Preferirei pensare proprio come il meccanismo "Metodi di estensione".

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

0

Questa è una variazione sui metodi di cui sopra. Le differenze sono che ci sono metodi statici sulla classe Car che corrispondono ai nomi dei metodi sul Builder, quindi non è necessario creare esplicitamente un Builder:

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

È possibile utilizzare gli stessi nomi di metodo utilizzati nelle chiamate del builder concatenato:

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

Inoltre, esiste un metodo .copy () sulla classe che restituisce un builder popolato con tutti i valori dall'istanza corrente, in modo da poter creare una variazione su un tema:

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

Infine, il metodo .build () del builder verifica che tutti i valori richiesti siano stati forniti e genera eventuali errori. Potrebbe essere preferibile richiedere alcuni valori sul costruttore del costruttore e consentire che il resto sia facoltativo; in tal caso, vorresti uno dei modelli 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);
        }
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.