Ao usar o encadeamento de métodos, reutilizo o objeto ou crio um?

37

Ao usar o encadeamento de métodos, como:

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

pode haver duas abordagens:

  • Reutilize o mesmo objeto, assim:

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
  • Crie um novo objeto do tipo Cara cada etapa, como este:

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

O primeiro está errado ou é uma escolha pessoal do desenvolvedor?


Acredito que a primeira abordagem possa causar rapidamente o código intuitivo / enganoso. Exemplo:

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

Alguma ideia?

Arseni Mourzenko
fonte
11
O que há de errado var car = new Car(Brand.Ford, 12345, Color.Silver);?
25412 James
12
@ Construtor telescópico do James, o padrão fluente pode ajudar a distinguir entre parâmetros opcionais e necessários (se eles são argumentos do construtor, se não forem opcionais). E o fluente é bastante agradável de ler.
NimChimpsky
8
@NimChimpsky, o que aconteceu com as boas propriedades à moda antiga (para C #) e um construtor que possui os campos necessários - não que eu esteja explorando APIs fluentes, sou um grande fã, mas eles geralmente são usados ​​em excesso
Chris S
8
@ ChrisS se você depende de setters (eu sou de java), você precisa tornar seus objetos mutáveis, o que você pode não querer fazer. E você também fica mais agradável com o intellitext ao usar fluente - requer menos reflexão, o ide quase constrói seu objeto para você.
NimChimpsky 25/05
11
@NimChimpsky yeh eu posso ver como fluente é um grande salto em frente para Java
Chris S

Respostas:

41

Eu colocaria a API fluente em sua própria classe "builder" separada do objeto que está criando. Dessa forma, se o cliente não quiser usar a API fluente, você ainda poderá usá-la manualmente e não poluir o objeto de domínio (aderindo ao princípio de responsabilidade única). Nesse caso, o seguinte seria criado:

  • Car qual é o objeto de domínio
  • CarBuilder que contém a API fluente

O uso seria assim:

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

A CarBuilderclasse ficaria assim (estou usando a convenção de nomenclatura C # aqui):

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

}

Observe que essa classe não será segura para threads (cada thread precisará de sua própria instância do CarBuilder). Observe também que, embora a API fluente seja um conceito muito interessante, provavelmente é um exagero com o objetivo de criar objetos de domínio simples.

Esse acordo é mais útil se você estiver criando uma API para algo muito mais abstrato e tiver configuração e execução mais complexas, e é por isso que funciona muito bem em testes de unidade e estruturas de DI. Você pode ver outros exemplos na seção Java do artigo da wikipedia Fluent Interface com objetos de persistência, manipulação de datas e simulação.


EDITAR:

Como observado nos comentários; você poderia tornar a classe Builder uma classe interna estática (dentro de Car) e Car poderia ser imutável. Este exemplo de deixar Car imutável parece um pouco tolo; mas em um sistema mais complexo, onde você absolutamente não deseja alterar o conteúdo do objeto que é criado, convém fazê-lo.

Abaixo está um exemplo de como fazer a classe interna estática e como lidar com uma criação de objeto imutável que ela constrói:

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

O uso seria o seguinte:

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 nos comentários fez um post no blog sobre o uso de construtores com funções lambda no contexto de gravação de testes de unidade com objetos de domínio complexos. É uma alternativa interessante para tornar o construtor um pouco mais expressivo.

No caso de CarBuildervocê precisar ter esse método:

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

Que pode ser usado assim:

Car c = CarBuilder
    .Build(car => 
        car.OfBrand(Brand.Ford)
           .OfModel(12345)
           .PaintedIn(Color.Silver);
Spoike
fonte
3
@Baqueta isso é descrito java eficaz de Bloch josh
Nim Chimpsky
6
@Baqueta leitura obrigatória para java dev, imho.
NimChimpsky
3
IMHO uma grande vantagem é que você pode usar esse padrão (se modificado adequadamente) para impedir que instâncias do objeto em construção que não foram concluídas escapem do construtor. Por exemplo, você pode garantir que não haverá carro com uma cor indefinida.
Scarfridge 25/05
11
Hmm ... sempre chamei o método final do padrão do construtor build()(ou Build()), não o nome do tipo que ele constrói ( Car()no seu exemplo). Além disso, se Carfor um objeto verdadeiramente imutável (por exemplo, todos os seus campos são readonly), mesmo o construtor não poderá modificá-lo, de modo que o Build()método se tornará responsável pela construção da nova instância. Uma maneira de fazer isso é Carter apenas um construtor, o que leva um construtor como argumento; então o Build()método pode apenas return new Car(this);.
Daniel Pryden
11
Eu fiz um blog sobre uma abordagem diferente para criar construtores baseados em lambdas. O post provavelmente precisa de um pouco de edição. Meu contexto era principalmente o de dentro do escopo de um teste de unidade, mas poderia ser aplicado a outras áreas também, se aplicável. Pode ser encontrado aqui: petesdotnet.blogspot.com/2012/05/…
Pete
9

Depende.

Seu carro é um objeto de entidade ou de valor ? Se o carro é uma entidade, a identidade do objeto é importante, portanto, você deve retornar a mesma referência. Se o objeto for um objeto de valor, ele deve ser imutável, o que significa que a única maneira é retornar uma nova instância sempre.

Um exemplo deste último seria a classe DateTime no .NET, que é um 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.

No entanto, se o modelo for uma entidade, eu gosto da resposta de Spoike sobre o uso de uma classe de construtor para criar seu objeto. Em outras palavras, esse exemplo que você deu apenas faz sentido IMHO se o carro for um objeto de valor.

Pete
fonte
11
+1 para a pergunta 'Entidade' vs 'Valor'. É uma questão de saber se sua classe é do tipo mutável ou imutável (esse objeto deve ser alterado?) E depende de você, embora isso afete seu design. Normalmente, eu não esperaria que o encadeamento de métodos funcionasse em um tipo mutável, a menos que o método retornasse um novo objeto.
Casey Kuball
6

Crie um construtor interno estático separado.

Use argumentos normais do construtor para os parâmetros necessários. E API fluente para opcional.

Não crie um novo objeto ao definir a cor, a menos que renomeie o método NewCarInColour ou algo semelhante.

Eu faria algo assim com a marca conforme necessário e o restante opcional (isso é java, mas o seu parece javascript, mas com certeza eles são intercambiáveis ​​com um pouco de nit nit):

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

O mais importante é que, seja qual for a decisão que você escolher, ela estará claramente declarada no nome e / ou comentário do método.

Não existe um padrão; algumas vezes o método retornará um novo objeto (a maioria dos métodos String o fará) ou retornará esse objeto para fins de encadeamento ou eficiência de memória.

Certa vez, projetei um objeto de vetor 3D e, para todas as operações matemáticas, tinha dois métodos implementados. Por instante, o 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
fonte
3
+1. Muito bom ponto. Realmente não vejo por que isso foi prejudicado. Observarei, no entanto, que os nomes que você escolheu não são muito claros. Eu os chamaria scale(o mutador) e scaledBy(o gerador).
back2dos
Bom ponto, nomes poderiam ter sido mais claros. A nomeação seguiu uma convenção de outras classes matemáticas que usei em uma biblioteca. O efeito também foi declarado nos comentários do método javadoc para evitar confusão.
XGouchet 25/05
3

Vejo aqui alguns problemas que acho confusos ... Sua primeira linha na pergunta:

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

Você está chamando um construtor (novo) e um método create ... Um método create () quase sempre seria um método estático ou um método construtor, e o compilador deve capturá-lo em um aviso ou erro para que você saiba, Dessa forma, essa sintaxe está errada ou tem nomes terríveis. Mas mais tarde, você não usa os dois, então vamos ver isso.

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

Novamente com o create, apenas não com um novo construtor. O problema é que acho que você está procurando um método copy (). Então, se esse for o caso, e é apenas um nome ruim, vejamos uma coisa ... você chama mercedes.Paintedin (Color.Yellow) .Copy () - Deve ser fácil olhar para isso e dizer que está sendo pintado 'antes de ser copiado - apenas um fluxo normal de lógica, para mim. Então coloque a cópia primeiro.

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

para mim, é fácil ver lá que você está pintando a cópia, fazendo o seu carro amarelo.

Drake Clarris
fonte
+1 para apontar a dissonância entre o novo e Create ();
Joshua Drake
1

A primeira abordagem tem a desvantagem que você mencionou, mas desde que você esclareça nos documentos, qualquer codificador semi-competente não deve ter problemas. Todo o código de encadeamento de métodos com o qual trabalhei pessoalmente funcionou dessa maneira.

A segunda abordagem obviamente tem a desvantagem de ser mais trabalhosa. Você também precisa decidir se as cópias que você devolve vão fazer cópias rasas ou profundas: o melhor pode variar de classe para classe ou método para método, para que você esteja introduzindo inconsistências ou comprometendo o melhor comportamento. Vale a pena notar que esta é a única opção para objetos imutáveis, como strings.

Faça o que fizer, não misture e combine na mesma classe!

vaughandroid
fonte
1

Prefiro pensar como o mecanismo "Extension Methods".

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

Esta é uma variação dos métodos acima. As diferenças são que existem métodos estáticos na classe Car que correspondem aos nomes dos métodos no Builder, portanto, você não precisa criar explicitamente um Builder:

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

É possível usar os mesmos nomes de método usados ​​nas chamadas do construtor em cadeia:

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

Além disso, existe um método .copy () na classe que retorna um construtor preenchido com todos os valores da instância atual, para que você possa criar uma variação em um tema:

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

Por fim, o método .build () do construtor verifica se todos os valores necessários foram fornecidos e lançados, se houver algum. Pode ser preferível exigir alguns valores no construtor do construtor e permitir que o restante seja opcional; nesse caso, você desejaria um dos padrões nas outras respostas.

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
fonte