Ao usar o método encadeamento, eu 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 Car em cada etapa, assim:

    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 é errado ou é uma escolha pessoal do desenvolvedor?

Eu acredito que a primeira abordagem pode rapidamente causar 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 opinião?

    
por Arseni Mourzenko 25.05.2012 / 10:54
fonte

8 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 pode usá-lo manualmente e não polui o objeto de domínio (aderindo ao princípio de responsabilidade única). Nesse caso, o seguinte seria criado:

  • Car , que é 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 classe CarBuilder 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 esta classe não será thread-safe (cada thread precisará de sua própria instância do CarBuilder). Observe também que, embora a API fluente seja um conceito muito legal, provavelmente é um exagero com o propósito de criar objetos de domínio simples.

Esta transação é 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 ela funciona muito bem em testes de unidade e estruturas de DI. Você pode ver alguns outros exemplos na seção Java do artigo da Fluid Interface da wikipedia com persistência, manuseio de data e objetos simulados .

EDITAR:

Como observado nos comentários; você poderia fazer da classe Builder uma classe interna estática (dentro do carro) e o carro poderia ser imutável. Este exemplo de deixar o carro ser imutável parece um pouco bobo; mas em um sistema mais complexo, onde você absolutamente não quer alterar o conteúdo do objeto que é construído, você pode querer 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 

Editar 2: Pete nos comentários feitos a postagem no blog sobre o uso de construtores com funções lambda no contexto da escrita de testes unitários com objetos de domínio complexos. É uma alternativa interessante para tornar o construtor um pouco mais expressivo.

No caso de CarBuilder , você precisa 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);
    
por 25.05.2012 / 11:16
fonte
9

Isso depende.

O seu carro é uma entidade ou um Objeto de valor ? Se o carro é uma entidade, então a identidade do objeto é importante, então você deve retornar a mesma referência. Se o objeto for um objeto de valor, ele deverá ser imutável, o que significa que a única maneira é retornar uma nova instância toda vez.

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 é uma entidade, eu gosto da resposta de Spoike ao usar uma classe de construtor para construir seu objeto. Em outras palavras, esse exemplo que você deu só faz sentido IMHO se o carro for um objeto de valor.

    
por 25.05.2012 / 13:33
fonte
6

Crie um construtor interno estático separado.

Use argumentos normais de construtor para os parâmetros requeridos. E api fluente para opcional.

Não crie um novo objeto ao definir cores, a menos que você renomeie o método NewCarInColour ou algo similar.

Eu faria algo assim com a marca conforme a necessidade e o restante como opcional (isso é o java, mas o seu parece com o javascript, mas tem certeza de que eles são intercambiáveis com um pouco de 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();
    
por 25.05.2012 / 11:31
fonte
4

O mais importante é que, seja qual for a decisão escolhida, é claramente indicado no nome do método e / ou comentário.

Não há padrão, às vezes o método retornará um novo objeto (a maioria dos métodos String o faz) ou retornará este objeto para fins de encadeamento ou para eficiência de memória).

Uma vez eu criei um objeto 3D Vector e para cada operação matemática eu tinha os 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);
}
    
por 25.05.2012 / 11:18
fonte
3

Eu vejo alguns problemas aqui que acho que podem ser 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 de criação ... Um método create () seria quase sempre um método estático ou um método de construtor, e o compilador deveria detectá-lo em um aviso ou erro para permitir que você sabe, de qualquer forma, esta sintaxe está errada ou tem nomes terríveis. Mas depois, 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, mas não com um novo construtor. A coisa é, acho que você está procurando um método copy (). Então, se esse é o caso, e é apenas um nome ruim, vamos ver 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.

    
por 25.05.2012 / 17:56
fonte
1
A primeira abordagem tem a desvantagem que você menciona, mas desde que você deixe claro nos documentos que qualquer codificador meio competente não deve ter problemas. Todo o código de encadeamento de métodos com o qual eu trabalhei pessoalmente funcionou dessa maneira.

A segunda abordagem obviamente tem a desvantagem de ser mais trabalho. Você também precisa decidir se as cópias retornadas farão cópias superficiais ou profundas: o que é melhor pode variar de classe para classe ou de método para método, assim você estará introduzindo inconsistência ou comprometendo o melhor comportamento. Vale a pena notar que esta é a única opção para objetos imutáveis, como strings.

Não importa o que você faça, não misture e combine dentro da mesma turma!

    
por 25.05.2012 / 11:18
fonte
1

Prefiro pensar apenas como o mecanismo "Métodos de extensão".

public Car PaintedIn(this Car car, Color color)
{
    car.Color = color;
    return car;
}
    
por 25.05.2012 / 23:24
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 Construtor, portanto, você não precisa criar explicitamente um Construtor:

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

Você pode usar os mesmos nomes de método usados nas chamadas do construtor encadeado:

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

Finalmente, o método .build () do construtor verifica se todos os valores requeridos foram fornecidos e lançados se algum estiver faltando. Pode ser preferível exigir alguns valores no construtor do construtor e permitir que o resto 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);
        }
    }
}
    
por 01.06.2012 / 03:44
fonte