Por que o encadeamento de setters é não convencional?

46

Ter o encadeamento implementado em beans é muito útil: não há necessidade de sobrecarregar construtores, mega construtores, fábricas e aumentar a legibilidade. Eu não consigo pensar em nenhuma desvantagem, a menos que você queira que seu objeto seja imutável , caso em que não teria nenhum setters de qualquer maneira. Então, há uma razão pela qual esta não é uma convenção OOP?

public class DTO {

    private String foo;
    private String bar;

    public String getFoo() {
         return foo;
    }

    public String getBar() {
        return bar;
    }

    public DTO setFoo(String foo) {
        this.foo = foo;
        return this;
    }

    public DTO setBar(String bar) {
        this.bar = bar;
        return this;
    }

}

//...//

DTO dto = new DTO().setFoo("foo").setBar("bar");
    
por Ben 02.02.2016 / 18:06
fonte

7 respostas

48

So is there a reason why isn't this a OOP convention?

Meu melhor palpite: porque viola CQS

Você tem um comando (alterando o estado do objeto) e uma consulta (retornando uma cópia do estado - neste caso, o próprio objeto) misturado no mesmo método. Isso não é necessariamente um problema, mas viola algumas das diretrizes básicas.

Por exemplo, em C ++, std :: stack :: pop () é um comando que retorna void e std :: stack :: top () é uma consulta que retorna uma referência ao elemento superior na pilha. Classicamente, você gostaria de combinar os dois, mas você não pode fazer isso e ser exceção segura. (Não é um problema em Java, porque o operador de atribuição em Java não lança).

Se o DTO for um tipo de valor, você poderá alcançar um fim semelhante com

public DTO setFoo(String foo) {
    return new DTO(foo, this.bar);
}

public DTO setBar(String bar) {
    return new DTO(this.foo, bar);
}

Além disso, os valores de retorno do encadeamento são uma dor colossal no momento em que você está lidando com a herança. Veja o "Padrão de modelo curiosamente recorrente"

Por fim, há o problema de o construtor padrão deixar você com um objeto que esteja em um estado válido. Se você precisar executar um monte de comandos para restaurar o objeto a um estado válido, algo está errado.

    
por 02.02.2016 / 18:51
fonte
32
  1. Salvar algumas combinações de teclas não é atraente. Pode ser interessante, mas as convenções de OOP se importam mais com conceitos e estruturas, não com pressionamentos de teclas.

  2. O valor de retorno não tem significado.

  3. Ainda mais do que não ter sentido, o valor de retorno é enganoso, pois os usuários podem esperar que o valor de retorno tenha significado. Eles podem esperar que seja um "setter imutável"

    public FooHolder {
        public FooHolder withFoo(int foo) {
            /* return a modified COPY of this FooHolder instance */
        }
    }
    

    Na realidade, seu setter muda o objeto.

  4. Não funciona bem com herança.

    public FooHolder {
        public FooHolder setFoo(int foo) {
            ...
        }
    }
    
    public BarHolder extends FooHolder {
        public FooHolder setBar(int bar) {
            ...
        }
    } 
    

    Eu posso escrever

    new BarHolder().setBar(2).setFoo(1)
    

    mas não

    new BarHolder().setFoo(1).setBar(2)
    

Para mim, de 1 a 3 são os mais importantes. Código bem escrito não é sobre texto agradavelmente organizado. Código bem escrito é sobre os conceitos fundamentais, relacionamentos e estrutura. O texto é apenas um reflexo externo do verdadeiro significado do código.

    
por 02.02.2016 / 21:37
fonte
12

Eu não acho que esta é uma convenção OOP, está mais relacionada ao design da linguagem e suas convenções.

Parece que você gosta de usar o Java. Java tem uma especificação JavaBeans que especifica o tipo de retorno do setter a ser anulado , isto é, está em conflito com o encadeamento de setters. Esta especificação é amplamente aceita e implementada em uma variedade de ferramentas.

É claro que você pode perguntar por que não está encadeando parte da especificação. Eu não sei a resposta, talvez esse padrão não seja conhecido / popular na época.

    
por 02.02.2016 / 18:19
fonte
7

Como outras pessoas disseram, isso geralmente é chamado de interface fluente .

Normalmente, os configuradores são chamados em variáveis em resposta ao código lógica em um aplicativo; sua classe DTO é um exemplo disso. Código convencional quando os setters não retornam nada é o melhor para isso. Outras respostas explicaram o caminho.

No entanto, há poucos casos em que a interface fluente pode ser uma boa solução, que eles têm em comum.

  • As constantes são passadas para os iniciantes
  • A lógica do programa não altera o que é passado para os setters.

Configuração de configuração, por exemplo nhibernate fluente

Id(x => x.Id);
Map(x => x.Name)
   .Length(16)
   .Not.Nullable();
HasMany(x => x.Staff)
   .Inverse()
   .Cascade.All();
HasManyToMany(x => x.Products)
   .Cascade.All()
   .Table("StoreProduct");

Configuração de dados de teste em testes de unidade, usando TestDataBulderClasses (Mães-objeto) especiais

members = MemberBuilder.CreateList(4)
    .TheFirst(1).With(b => b.WithFirstName("Rob"))
    .TheNext(2).With(b => b.WithFirstName("Poya"))
    .TheNext(1).With(b => b.WithFirstName("Matt"))
    .BuildList(); // Note the "build" method sets everything else to
                  // senible default values so a test only need to define 
                  // what it care about, even if for example a member 
                  // MUST have MembershipId  set

No entanto, criar uma interface fluente é muito difícil , por isso só vale a pena quando você tem muita configuração "estática". Também interface fluente não deve ser misturada com classes “normais”; daí o padrão de construtor é frequentemente usado.

    
por 03.02.2016 / 11:32
fonte
5

Eu acho que a razão pela qual não é uma convenção para encadear um setter após o outro é porque, para esses casos, é mais comum ver um objeto de opções ou parâmetros em um construtor. C # também tem uma sintaxe inicializadora.

Em vez de:

DTO dto = new DTO().setFoo("foo").setBar("bar");

Pode-se escrever:

(em JS)

var dto = new DTO({foo: "foo", bar: "bar"});

(em c #)

DTO dto = new DTO{Foo = "foo", Bar = "bar"};

(em Java)

DTO dto = new DTO("foo", "bar");

setFoo e setBar não são mais necessários para a inicialização e podem ser usados para mutação mais tarde.

Embora a capacidade de cadeia seja útil em algumas circunstâncias, é importante não tentar colocar tudo em uma única linha apenas para reduzir os caracteres da nova linha.

Por exemplo

dto.setFoo("foo").setBar("fizz").setFizz("bar").setBuzz("buzz");

torna mais difícil ler e entender o que está acontecendo. Reformatando para:

dto.setFoo("foo")
    .setBar("fizz")
    .setFizz("bar")
    .setBuzz("buzz");

É muito mais fácil de entender e torna o "erro" na primeira versão mais óbvio. Depois de refatorar o código para esse formato, não há vantagem real sobre:

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");
    
por 02.02.2016 / 18:40
fonte
5

Essa técnica é realmente usada no padrão Builder.

x = ObjectBuilder()
        .foo(5)
        .bar(6);

No entanto, em geral, é evitado porque é ambíguo. Não é óbvio se o valor de retorno é o objeto (assim você pode chamar outros setters) ou se o objeto de retorno é o valor que acabou de ser designado (também um padrão comum). Assim, o Princípio de menor surpresa sugere que você não deve tentar assumir que o usuário deseja ver uma solução ou outra, a menos que seja fundamental para o design do objeto.

    
por 03.02.2016 / 04:46
fonte
2

Este é mais um comentário do que uma resposta, mas não posso comentar, então ...

só queria mencionar que essa pergunta me surpreendeu porque não vejo isso como incomum . Na verdade, no meu ambiente de trabalho (desenvolvedor web) é muito comum.

Por exemplo, é assim que a doutrina do symfony: generate: entities command gera automaticamente all setters , por padrão .

jQuery meio que acorrenta a maioria de seus métodos de uma forma muito semelhante.

    
por 04.02.2016 / 11:27
fonte