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


46

Ter o encadeamento implementado nos beans é muito útil: não é necessário sobrecarregar construtores, mega construtores, fábricas e oferece maior legibilidade. Não consigo pensar em nenhuma desvantagem, a menos que você queira que seu objeto seja imutável ; nesse caso, não haveria nenhum levantador de qualquer maneira. Então, existe uma razão para que essa não seja 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");

32
Porque Java pode ser a única língua em que setters não é abominável homem ...
Telastyn

11
É um estilo ruim porque o valor de retorno não é semanticamente significativo. Isso é enganador. A única vantagem é economizar muito poucas teclas.
usr

58
Esse padrão não é incomum. Existe até um nome para isso. Chama-se interface fluente .
Philipp

9
Você também pode abstrair essa criação em um construtor para obter um resultado mais legível. myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();Eu faria isso, para não entrar em conflito com a idéia geral de que levantadores são vazios.

9
@ Philipp, embora tecnicamente você esteja certo, não diria que new Foo().setBar('bar').setBaz('baz')parece muito "fluente". Quero dizer, com certeza ele poderia ser implementado exatamente da mesma maneira, mas eu esperaria muito ler algo mais parecido comFoo().barsThe('bar').withThe('baz').andQuuxes('the quux')
Wayne Werner

Respostas:


50

Então, existe uma razão pela qual essa convenção não é uma OOP?

Meu melhor palpite: porque viola o CQS

Você tem um comando (alterando o estado do objeto) e uma consulta (retornando uma cópia do estado - nesse caso, o próprio objeto) misturados 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 nulo, e std :: stack :: top () é uma consulta que retorna uma referência ao elemento top na pilha. Classicamente, você gostaria de combinar os dois, mas não pode fazer isso e ter uma exceção segura. (Não é um problema em Java, porque o operador de atribuição em Java não lança).

Se o DTO fosse um tipo de valor, você poderia obter um final 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 quando você está lidando com herança. Consulte o "Padrão de modelo curiosamente recorrente"

Finalmente, há o problema de que o construtor padrão deve deixá-lo com um objeto que esteja em um estado válido. Se você precisar executar vários comandos para restaurar o objeto para um estado válido, algo deu muito errado.


4
Finalmente, uma verdadeira catain aqui. A herança é o verdadeiro problema. Eu acho que o encadeamento poderia ser um setter convencional para objetos de representação de dados que são usados ​​na composição.
Ben

6
"retornando uma cópia do estado de um objeto" - não faz isso.
user253751

2
Eu argumentaria que o maior motivo para seguir essas diretrizes (com algumas exceções notáveis, como os iteradores) é que isso facilita muito o raciocínio do código .
Jpmc26

1
@MatthieuM. Não. Eu falo principalmente Java, e é predominante nas APIs "fluidas" avançadas - tenho certeza de que também existe em outros idiomas. Essencialmente, você torna seu tipo genérico em um tipo que se estende. Você adiciona um abstract T getSelf()método com retorna o tipo genérico. Agora, em vez de retornar thisde seus setters, você return getSelf()e qualquer classe substituindo simplesmente faz o tipo genérico em si e volta thisde getSelf. Dessa forma, os setters retornam o tipo real e não o tipo declarante.
Boris, o Aranha

1
@MatthieuM. realmente? Soa semelhante ao CRTP para C ++ ...
Inútil

33
  1. Salvar algumas teclas não é convincente. Pode ser bom, mas as convenções de OOP se preocupam mais com conceitos e estruturas, não com pressionamentos de teclas.

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

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

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

    Na realidade, o 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 consigo escrever

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

    mas não

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

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


2
A maioria desses argumentos se aplica a qualquer interface fluente / encadeada.
Casey

@ Casey, sim. Construtores (ou seja, com setters) é o caso mais que vejo do encadeamento.
Paul Draper

10
Se você conhece a idéia de uma interface fluente, o nº 3 não se aplica, pois é provável que você suspeite de uma interface fluente do tipo de retorno. Eu também tenho que discordar de "Código bem escrito não é sobre texto agradavelmente organizado". Não é só isso, mas o texto bem organizado é bastante importante, pois permite ao leitor humano do programa entendê-lo com menos esforço.
Michael Shaw

1
O encadeamento do setter não consiste em organizar o texto de maneira agradável ou em salvar pressionamentos de tecla. Trata-se de reduzir o número de identificadores. Com o encadeamento de setter, você pode criar e configurar o objeto em uma única expressão, o que significa que talvez você não precise salvá-lo em uma variável - uma variável que você precisará nomear e que permanecerá lá até o final do escopo.
Idan Arye 03/02

3
Sobre o ponto 4, é algo que pode ser resolvido em Java da seguinte maneira: public class Foo<T extends Foo> {...}com o setter retornando Foo<T>. Também esses métodos 'setter', prefiro chamá-los de 'com' métodos. Se a sobrecarga funcionar bem, apenas o Foo<T> with(Bar b) {...}contrário Foo<T> withBar(Bar b).
YoYo 04/02

12

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

Parece que você gosta de usar Java. Java possui uma especificação JavaBeans que especifica o tipo de retorno do configurador a ser anulado, ou seja, está em conflito com o encadeamento de configuradores. 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. Não sei a resposta, talvez esse padrão não fosse conhecido / popular na época.


Sim, JavaBeans é exatamente o que eu tinha em mente.
Ben

Eu não acho que a maioria dos aplicativos que usa JavaBeans se importa, mas eles podem (usando a reflexão para capturar os métodos chamados 'set ...', têm um único parâmetro e são nulos de retorno ). Muitos programas de análise estática reclamam de não verificar um valor de retorno de um método que retorna algo e isso pode ser muito irritante.

As chamadas reflexivas do @MichaelT para obter métodos em Java não especificam o tipo de retorno. Chamar um método dessa maneira retorna Object, o que pode resultar no retorno do singleton do tipo Voidse o método especificar voidcomo seu tipo de retorno. Em outras notícias, aparentemente "deixar um comentário, mas não votar" é motivo para falhar na auditoria do "primeiro post", mesmo que seja um bom comentário. Eu culpo o shog por isso.

7

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

Normalmente, os setters são variáveis ​​de passagem de chamadas em resposta ao código lógico em um aplicativo; sua classe de DTO é um exemplo disso. O código convencional quando os setters não retornam nada é o melhor para isso. Outras respostas explicaram o caminho.

No entanto, existem alguns casos em que a interface fluente pode ser uma boa solução, estes têm em comum.

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

Definindo a configuração, por exemplo, fluent-nhibernate

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

Configurando dados de teste em testes de unidade, usando TestDataBulderClasses especial (mães de objeto)

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, a criação de uma interface fluente boa é muito difícil , por isso só vale a pena quando você tem muita configuração "estática". Também a interface fluente não deve ser misturada às classes "normais"; portanto, o padrão do construtor é frequentemente usado.


5

Eu acho que muito do motivo de não ser uma convenção encadear um setter após o outro é porque, nesses casos, é mais comum ver um objeto de opções ou parâmetros em um construtor. O C # também possui uma sintaxe de inicializador.

Ao invés de:

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

Alguém pode 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");

setFooe não setBarsão mais necessários para a inicialização e podem ser usados ​​para mutação posteriormente.

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

Por exemplo

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

torna mais difícil ler e entender o que está acontecendo. Reformatação 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");

3
1. Eu realmente não posso concordar com a questão da legibilidade, adicionando uma instância antes de cada chamada não estar tornando tudo mais claro. 2. Os padrões de inicialização que você mostrou com js e C # não têm nada a ver com construtores: o que você fez em js é passado um único argumento, e o que você fez em c # é um shugar de sintaxe que chama geters e setter nos bastidores e java não possui o shugar geter-setter como o C #.
Ben

1
@Benedictus, eu estava mostrando várias maneiras diferentes pelas quais as linguagens OOP lidam com esse problema sem encadear. O objetivo não era fornecer código idêntico, o objetivo era mostrar alternativas que tornam o encadeamento desnecessário.
ZzzzBov

2
"adicionar instância antes de cada chamada não está tornando tudo mais claro" Eu nunca afirmei que adicionar a instância antes de cada chamada tornasse algo mais claro, eu apenas disse que era relativamente equivalente.
ZzzzBov

1
O VB.NET também possui uma palavra-chave "With" utilizável, que cria uma referência temporária do compilador para que, por exemplo, [use /to represente quebra de linha] With Foo(1234) / .x = 23 / .y = 47seja equivalente a Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47. Essa sintaxe não cria ambiguidade, pois, .xpor si só, não pode ter outro significado senão vincular-se à declaração "With" imediatamente circundante [se não houver, ou se o objeto não tiver membro x, então não terá .xsentido]. A Oracle não incluiu nada disso em Java, mas essa construção se encaixaria perfeitamente na linguagem.
Supercat 03/02

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 (para que você possa chamar outros setters) ou se o objeto de retorno é o valor que acabou de ser atribuído (também um padrão comum). Assim, o Princípio da 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.


6
A diferença é que, ao usar o padrão do construtor, você normalmente escreve um objeto somente para gravação (o Construtor) que eventualmente constrói um objeto somente leitura (imutável) (qualquer classe que estiver construindo). Nesse sentido, é desejável ter uma longa cadeia de chamadas de método, pois pode ser usado como uma expressão única.
Darkhogg

"ou se o objeto de retorno é o valor que acabou de ser atribuído" Eu odeio isso, se eu quiser manter o valor que acabei de passar, colocarei em uma variável primeiro. Porém, pode ser útil obter o valor que foi atribuído anteriormente (algumas interfaces de contêiner fazem isso, acredito).
JAB

@JAB Eu concordo, não sou tão fã dessa notação, mas ela tem seus lugares. O que vem à mente é Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); que acho que também ganhou popularidade porque reflete a = b = c = d, embora não esteja convencido de que a popularidade tenha sido bem fundamentada. Eu vi o que você mencionou, retornando o valor anterior, em algumas bibliotecas de operações atômicas. Moral da história? É ainda mais confuso do que eu fez soar =)
Cort Ammon

Acredito que os acadêmicos ficam um pouco pedantes: não há nada inerentemente errado com o idioma. É extremamente útil para adicionar clareza em certos casos. Pegue a seguinte classe de formatação de texto, por exemplo, que recua todas as linhas de uma seqüência de caracteres e desenha uma caixa de arte ASCII ao seu redor new BetterText(string).indent(4).box().print();. Nesse caso, eu ignorei um monte de bobagens e peguei uma corda, recuei, encaixotei e saí. Você pode até querer um método de duplicação dentro da cascata (como, por exemplo, .copy()) para permitir que todas as ações a seguir não modifiquem mais o original.
Tgm1024

2

Isso é 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 de maneira incomum . Na verdade, no meu ambiente de trabalho (desenvolvedor web) é muito, muito comum.

Por exemplo, é assim que o comando doutrina: generate: entity do Symfony gera automaticamente tudo setters , por padrão .

O jQuery meio que encadeia a maioria de seus métodos de uma maneira muito semelhante.


Js é uma abominação
Ben

@Benedictus Eu diria que o PHP é a maior abominação. O JavaScript é uma linguagem perfeitamente adequada e se tornou bastante agradável com a inclusão dos recursos do ES6 (embora eu tenha certeza de que algumas pessoas ainda preferem o CoffeeScript ou variantes; pessoalmente, não sou fã de como o CoffeeScript lida com o escopo de variáveis ​​ao verificar o escopo externo primeiro, em vez de tratar variáveis ​​atribuídas no escopo local como local, a menos que seja explicitamente indicado como não local / global da maneira que o Python o faz).
JAB

@Benedictus concordo com JAB. Nos meus anos de faculdade, eu costumava olhar para JS como uma linguagem de script de abominação, mas depois de trabalhar alguns anos com ela e aprender a maneira correta de usá-la, cheguei a ... realmente adorá- la . E acho que com o ES6 se tornará uma linguagem realmente madura . Mas, o que me faz virar a cabeça é ... o que minha resposta tem a ver com JS em primeiro lugar? ^^ U
xDaizu

Na verdade, trabalhei alguns anos com js e posso dizer que aprendi suas maneiras. Não obstante, vejo essa linguagem como nada mais que um erro. Mas eu concordo, Php é muito pior.
187 Ben Ben

@Benedictus Entendo sua posição e a respeito. Mesmo assim, gosto. Claro, ele tem suas peculiaridades e vantagens (que idioma não tem?), Mas está constantemente avançando na direção certa. Mas ... Na verdade, eu não gosto tanto que preciso defendê-lo. Eu não ganho nada com as pessoas que amam ou odeiam ... hahaha Além disso, este não é o lugar para isso, minha resposta nem sequer foi sobre JS.
XDaizu
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.