Cuando uso el método de encadenamiento, ¿reutilizo el objeto o creo uno?

37

Cuando se utiliza el método de encadenamiento como:

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

Puede haber dos enfoques:

  • Reutilice el mismo objeto, así:

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
  • Cree un nuevo objeto de tipo Caren cada paso, así:

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

¿El primero es incorrecto o es más bien una elección personal del desarrollador?


Creo que el primer enfoque puede causar rápidamente el código intuitivo / engañoso. Ejemplo:

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

¿Alguna idea?

Arseni Mourzenko
fuente
1
¿Qué tiene de malo var car = new Car(Brand.Ford, 12345, Color.Silver);?
James
12
@James, constructor telescópico, el patrón fluido puede ayudar a distinguir entre parámetros opcionales y requeridos (si son argumentos de constructor requeridos, si no es opcional). Y el fluido es bastante agradable de leer.
NimChimpsky
8
@NimChimpsky lo que sucedió con las buenas propiedades anticuadas (para C #) y un constructor que tiene los campos que se requieren, no es que esté explotando APIs fluidas, soy un gran admirador pero a menudo se usan en exceso
Chris S
8
@ChrisS si confías en setters (yo soy de Java) tienes que hacer que tus objetos sean mutables, lo que quizás no quieras hacer. Y también obtienes un texto inteligente más agradable cuando usas con fluidez: requiere menos pensamiento, el ide casi construye tu objeto para ti.
NimChimpsky
1
@NimChimpsky yeh Puedo ver cuán fluido es un gran salto adelante para Java
Chris S

Respuestas:

41

Pondría la API fluida a su propia clase "constructora" separada del objeto que está creando. De esa manera, si el cliente no quiere usar la API fluida, aún puede usarla manualmente y no contamina el objeto de dominio (adhiriéndose al principio de responsabilidad única). En este caso se crearía lo siguiente:

  • Car cual es el objeto de dominio
  • CarBuilder que contiene la API fluida

El uso sería así:

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

La CarBuilderclase se vería así (estoy usando la convención de nomenclatura de C # aquí):

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

}

Tenga en cuenta que esta clase no será segura para subprocesos (cada subproceso necesitará su propia instancia de CarBuilder). También tenga en cuenta que, aunque la API fluida es un concepto realmente genial, probablemente sea excesivo con el fin de crear objetos de dominio simples.

Este acuerdo es más útil si está creando una API para algo mucho más abstracto y tiene una configuración y ejecución más compleja, por lo que funciona muy bien en pruebas unitarias y marcos DI. Puedes ver algunos otros ejemplos en la sección Java del artículo de Wikipedia sobre la interfaz fluida con persistencia, manejo de fechas y objetos simulados.


EDITAR:

Como se señaló en los comentarios; podría hacer que la clase Builder sea una clase interna estática (dentro de Car) y Car podría hacerse inmutable. Este ejemplo de dejar que el coche sea inmutable parece un poco tonto; pero en un sistema más complejo, donde absolutamente no desea cambiar el contenido del objeto que se construye, es posible que desee hacerlo.

A continuación se muestra un ejemplo de cómo hacer la clase interna estática y cómo manejar una creación de objeto inmutable que construye:

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

El uso sería el siguiente:

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

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

Edición 2: Pete en los comentarios hizo una publicación de blog sobre el uso de constructores con funciones lambda en el contexto de escribir pruebas unitarias con objetos de dominio complejos. Es una alternativa interesante para hacer que el constructor sea un poco más expresivo.

En el caso de CarBuilderque necesite tener este método en su lugar:

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

Que se puede usar así:

Car c = CarBuilder
    .Build(car => 
        car.OfBrand(Brand.Ford)
           .OfModel(12345)
           .PaintedIn(Color.Silver);
Spoike
fuente
3
@Baqueta, esto se describe como el efectivo java de josh bloch
NimChimpsky
66
@Baqueta requirió lectura para java dev, en mi humilde opinión.
NimChimpsky
3
En mi humilde opinión, una gran ventaja es que puede usar este patrón (si se modifica adecuadamente) para evitar que las instancias del objeto en construcción que no se completan escapen del generador. Por ejemplo, puede asegurarse de que no habrá un automóvil con un color indefinido.
scarfridge
1
Hmm ... Siempre he llamado el método final del patrón de construcción build()(o Build()), no el nombre del tipo que construye ( Car()en su ejemplo). Además, si Cares un objeto verdaderamente inmutable (por ejemplo, todos sus campos lo son readonly), incluso el constructor no podrá mutarlo, por lo que el Build()método se hace responsable de construir la nueva instancia. Una forma de hacer esto es tener Carun solo constructor, que tome un Constructor como argumento; entonces el Build()método puede simplemente return new Car(this);.
Daniel Pryden
1
Hice un blog sobre un enfoque diferente para crear constructores basados ​​en lambdas. La publicación probablemente necesita un poco de edición. Mi contexto era principalmente el del alcance de una prueba unitaria, pero también podría aplicarse a otras áreas si corresponde. Se puede encontrar aquí: petesdotnet.blogspot.com/2012/05/…
Pete
9

Eso depende.

¿Es su automóvil una entidad o un objeto de valor ? Si el automóvil es una entidad, entonces la identidad del objeto es importante, por lo que debe devolver la misma referencia. Si el objeto es un objeto de valor, debe ser inmutable, lo que significa que la única forma es devolver una nueva instancia cada vez.

Un ejemplo de esto último sería la clase DateTime en .NET, que es un objeto de valor.

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.

Sin embargo, si el modelo es una entidad, me gusta la respuesta de Spoike sobre el uso de una clase de constructor para construir su objeto. En otras palabras, ese ejemplo que diste solo tiene sentido en mi humilde opinión si el coche es un objeto de valor.

Pete
fuente
1
+1 para la pregunta 'Entidad' vs 'Valor'. Es una cuestión de si su clase es de tipo mutable o inmutable (¿debería cambiarse este objeto?) Y depende completamente de usted, aunque afectará su diseño. Normalmente no esperaría que el encadenamiento de métodos funcione en un tipo mutable, a menos que el método devuelva un nuevo objeto.
Casey Kuball
6

Cree un generador interno estático separado.

Utilice argumentos de constructor normales para los parámetros requeridos. Y api fluido para opcional.

No cree un nuevo objeto al configurar el color, a menos que cambie el nombre del método NewCarInColour o algo similar.

Haría algo como esto con la marca según sea necesario y el resto opcional (esto es java, pero el suyo parece javascript, pero bastante seguro de que son intercambiables con un poco de selección de liendres):

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
fuente
4

Lo más importante es que, sea cual sea la decisión que elija, se indica claramente en el nombre y / o comentario del método.

No hay un estándar, a veces el método devolverá un nuevo objeto (la mayoría de los métodos de String lo hacen) o lo devolverá para encadenar o para mejorar la memoria).

Una vez diseñé un objeto Vector 3D y para cada operación matemática tenía implementados ambos métodos. Por instante el método de escala:

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
fuente
3
+1. Muy buen punto. Realmente no veo por qué esto recibió un voto negativo. Sin embargo, notaré que los nombres que elegiste no son muy claros. Los llamaría scale(el mutador) y scaledBy(el generador).
back2dos
Buen punto, los nombres podrían haber sido más claros. El nombramiento siguió una convención de otras clases matemáticas que utilicé de una biblioteca. El efecto también se afirmó en los comentarios javadoc del método para evitar confusiones.
XGouchet
3

Veo algunos problemas aquí que creo que pueden ser confusos ... Su primera línea en la pregunta:

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

Está llamando a un constructor (nuevo) y a un método de creación ... Un método de creación () casi siempre sería un método estático o un método de creación, y el compilador debería detectarlo en una advertencia o error para informarle, ya sea De esta manera, esta sintaxis es incorrecta o tiene algunos nombres terribles. Pero más adelante, no usas ambos, así que veamos eso.

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

Sin embargo, de nuevo con create, solo que no con un nuevo constructor. La cosa es que creo que estás buscando un método copy () en su lugar. Entonces, si ese es el caso, y es solo un mal nombre, veamos una cosa ... que llamas mercedes.Paintedin (Color.Yellow) .Copy () - Debería ser fácil ver eso y decir que está siendo 'pintado' 'antes de ser copiado, solo un flujo normal de lógica, para mí. Así que pon la copia primero.

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

para mí, es fácil ver que estás pintando la copia, haciendo tu auto amarillo.

Drake Clarris
fuente
+1 para señalar la disonancia entre el nuevo y Create ();
Joshua Drake
1

El primer enfoque tiene el inconveniente que usted menciona, pero siempre que lo deje claro en los documentos, cualquier codificador medio competente no debería tener problemas. Todo el código de encadenamiento de métodos con el que he trabajado personalmente ha funcionado de esta manera.

El segundo enfoque obviamente tiene el inconveniente de ser más trabajo. También debe decidir si las copias que devuelve serán copias superficiales o profundas: lo que es mejor puede variar de una clase a otra o de un método a otro, por lo que introducirá inconsistencia o comprometerá el mejor comportamiento. Vale la pena señalar que esta es la única opción para objetos inmutables, como cadenas.

Hagas lo que hagas, no mezclar y combinar en la misma clase!

vaughandroid
fuente
1

Prefiero pensar como el mecanismo de "Métodos de extensión".

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

Esta es una variación de los métodos anteriores. Las diferencias son que hay métodos estáticos en la clase Car que coinciden con los nombres de los métodos en el Generador, por lo que no necesita crear explícitamente un Generador:

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

Puede usar los mismos nombres de método que usa en las llamadas del generador encadenado:

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

Además, hay un método .copy () en la clase que devuelve un generador rellenado con todos los valores de la instancia actual, por lo que puede crear una variación sobre un tema:

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

Finalmente, el método .build () del constructor verifica que se hayan proporcionado todos los valores requeridos y arroja si falta alguno. Puede ser preferible requerir algunos valores en el constructor del constructor y permitir que el resto sea opcional; en ese caso, querrías uno de los patrones en las otras respuestas.

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
fuente