Eu já vi muitas implementações do padrão Builder (principalmente em Java). Todos eles têm uma classe de entidade (digamos, uma Person
classe) e uma classe de construtor PersonBuilder
. O construtor "empilha" uma variedade de campos e retorna a new Person
com os argumentos passados. Por que precisamos explicitamente de uma classe de construtor, em vez de colocar todos os métodos de construtor na Person
própria classe?
Por exemplo:
class Person {
private String name;
private Integer age;
public Person() {
}
Person withName(String name) {
this.name = name;
return this;
}
Person withAge(int age) {
this.age = age;
return this;
}
}
Eu posso simplesmente dizer Person john = new Person().withName("John");
Por que a necessidade de uma PersonBuilder
aula?
O único benefício que vejo é que podemos declarar os Person
campos como final
, garantindo assim a imutabilidade.
java
design-patterns
builder-pattern
Boyan Kushlev
fonte
fonte
chainable setters
: DwithName
devolver uma cópia da Pessoa com apenas o campo de nome alterado. Em outras palavras,Person john = new Person().withName("John");
poderia funcionar mesmo quePerson
seja imutável (e esse é um padrão comum na programação funcional).void
métodos. Portanto, por exemplo, sePerson
houver um método que imprima o nome deles, você ainda poderá encadear uma interface fluenteperson.setName("Alice").sayName().setName("Bob").sayName()
. A propósito, eu anoto aqueles no JavaDoc exatamente com a sua sugestão@return Fluent interface
- é genérico e claro o suficiente quando se aplica a qualquer método que façareturn this
no final de sua execução e é bastante claro. Portanto, um construtor também fará uma interface fluente.Respostas:
É para que você possa ser imutável E simular parâmetros nomeados ao mesmo tempo.
Isso mantém suas luvas longe da pessoa até que seu estado seja definido e, uma vez definido, não permitirá que você o altere, mas todos os campos são claramente rotulados. Você não pode fazer isso com apenas uma classe em Java.
Parece que você está falando sobre Josh Blochs Builder Pattern . Isso não deve ser confundido com o padrão do grupo dos quatro construtores . Estes são animais diferentes. Ambos resolvem problemas de construção, mas de maneiras bastante diferentes.
Claro que você pode construir seu objeto sem usar outra classe. Mas então você tem que escolher. Você perde a capacidade de simular parâmetros nomeados em linguagens que não os possuem (como Java) ou a capacidade de permanecer imutável durante toda a vida útil dos objetos.
Exemplo imutável, não tem nomes para parâmetros
Aqui você está construindo tudo com um único construtor simples. Isso permitirá que você permaneça imutável, mas você perde a simulação dos parâmetros nomeados. Isso fica difícil de ler com muitos parâmetros. Os computadores não se importam, mas é difícil para os humanos.
Exemplo de parâmetro nomeado simulado com setters tradicionais. Não é imutável.
Aqui você cria tudo com setters e simula parâmetros nomeados, mas não é mais imutável. Cada uso de um setter altera o estado do objeto.
Então, o que você obtém adicionando a classe é que você pode fazer as duas coisas.
A validação pode ser realizada
build()
se um erro de tempo de execução para um campo de idade ausente for suficiente para você. Você pode atualizar isso e aplicar issoage()
chamado com um erro do compilador. Apenas não com o padrão do construtor Josh Bloch.Para isso, você precisa de uma linguagem específica de domínio interno (iDSL).
Isso permite exigir que eles liguem
age()
ename()
antes de ligarbuild()
. Mas você não pode fazer isso apenas retornando athis
cada vez. Cada coisa que retorna retorna uma coisa diferente que força você a chamar a próxima coisa.O uso pode ficar assim:
Mas isso:
causa um erro do compilador porque
age()
só é válido chamar o tipo retornado porname()
.Esses iDSLs são extremamente poderosos ( JOOQ ou Java8 Streams, por exemplo) e são muito bons de usar, especialmente se você usar um IDE com conclusão de código, mas eles são um bom trabalho de configuração. Eu recomendo salvá-los para coisas que terão um bom código-fonte escrito contra eles.
fonte
AnonymousPersonBuilder
que tenha seu próprio conjunto de regras.Por que usar / fornecer uma classe de construtor:
fonte
PersonBuilder
não tem getters, e a única maneira de inspecionar os valores atuais é chamar.Build()
para retornar aPerson
. Dessa forma, o .Build pode validar um objeto corretamente construído, correto? Ou seja, o mecanismo destinado a impedir o uso de um objeto "em construção"?Build
operação para evitar objetos ruins e / ou subconstruídos.getAge
ao construtor poderá retornarnull
quando essa propriedade ainda não tiver sido especificada. Por outro lado, aPerson
classe pode impor o invariante de que a idade nunca pode sernull
, o que é impossível quando o construtor e o objeto real são misturados, como nonew Person() .withName("John")
exemplo do OP , que cria felizmente umPerson
sem idade. Isso se aplica mesmo a classes mutáveis, pois os setters podem impor invariantes, mas não podem impor valores iniciais.Um motivo seria garantir que todos os dados passados sigam as regras de negócios.
Seu exemplo não leva isso em consideração, mas digamos que alguém tenha passado uma string vazia ou uma string composta por caracteres especiais. Você gostaria de fazer algum tipo de lógica baseada em garantir que o nome deles seja realmente um nome válido (que é realmente uma tarefa muito difícil).
Você pode colocar tudo isso na sua classe Person, especialmente se a lógica for muito pequena (por exemplo, apenas garantir que uma idade não seja negativa), mas à medida que a lógica cresce, faz sentido separá-la.
fonte
john
Um ângulo um pouco diferente do que vejo em outras respostas.
A
withFoo
abordagem aqui é problemática porque eles se comportam como setters, mas são definidos de uma maneira que faz parecer que a classe suporta a imutabilidade. Nas classes Java, se um método modifica uma propriedade, é habitual iniciar o método com 'set'. Eu nunca amei isso como um padrão, mas se você fizer outra coisa, vai surpreender as pessoas e isso não é bom. Há outra maneira de oferecer suporte à imutabilidade com a API básica que você tem aqui. Por exemplo:Ele não fornece muita maneira de impedir objetos inadequadamente construídos parcialmente, mas evita alterações em qualquer objeto existente. Provavelmente é bobagem para esse tipo de coisa (e o JB Builder também). Sim, você criará mais objetos, mas isso não é tão caro quanto você imagina.
Você verá principalmente esse tipo de abordagem usada com estruturas de dados simultâneas, como CopyOnWriteArrayList . E isso sugere por que a imutabilidade é importante. Se você deseja tornar seu código seguro, a imutabilidade quase sempre deve ser considerada. Em Java, cada encadeamento tem permissão para manter um cache local de estado variável. Para que um thread veja as alterações feitas em outros threads, um bloco sincronizado ou outro recurso de simultaneidade precisa ser empregado. Qualquer um desses itens adicionará alguma sobrecarga ao código. Mas se suas variáveis são finais, não há nada a fazer. O valor sempre será o que foi inicializado e, portanto, todos os threads verão a mesma coisa, não importa o quê.
fonte
Outro motivo que ainda não foi explicitamente mencionado aqui é que o
build()
método pode verificar se todos os campos são 'contêm valores válidos (definidos diretamente ou derivados de outros valores de outros campos), que provavelmente é o modo de falha mais provável isso ocorreria de outra forma.Outro benefício é que seu
Person
objeto acabaria tendo uma vida útil mais simples e um conjunto simples de invariantes. Você sabe que depois de ter umPerson p
, você tem ump.name
e um válidop.age
. Nenhum dos seus métodos precisa ser projetado para lidar com situações como "bem, se a idade for definida, mas não o nome, ou se o nome for definido, mas não a idade?" Isso reduz a complexidade geral da classe.fonte
URI
, umFile
, ou umaFileInputStream
, e usos o que estava previsto para obter umaFileInputStream
que, finalmente, vai para a chamada do construtor como um argumentoUm construtor também pode ser definido para retornar uma interface ou uma classe abstrata. Você pode usar o construtor para definir o objeto, e o construtor pode determinar qual subclasse concreta retornar com base em quais propriedades estão definidas ou em que estão definidas, por exemplo.
fonte
O padrão Builder é usado para construir / criar o objeto passo a passo , definindo as propriedades e, quando todos os campos obrigatórios estiverem definidos, retorne o objeto final usando o método build. O objeto recém-criado é imutável. O ponto principal a ser observado aqui é que o objeto só é retornado quando o método de construção final é chamado. Isso garante que todas as propriedades sejam configuradas para o objeto e, portanto, o objeto não estará em estado inconsistente quando retornado pela classe do construtor.
Se não usarmos a classe construtor e colocarmos diretamente todos os métodos da classe construtor na classe Person, primeiro precisamos criar o Object e, em seguida, chamar os métodos setter no objeto criado, o que levará ao estado inconsistente do objeto entre a criação. do objeto e definindo as propriedades.
Assim, usando a classe builder (ou seja, alguma entidade externa que não seja a própria classe Person), garantimos que o objeto nunca estará no estado inconsistente.
fonte
Reutilize o objeto construtor
Como outros já mencionaram, a imutabilidade e a verificação da lógica de negócios de todos os campos para validar o objeto são os principais motivos para um objeto construtor separado.
No entanto, a reutilização é outro benefício. Se eu quiser instanciar muitos objetos muito semelhantes, posso fazer pequenas alterações no objeto do construtor e continuar instanciando. Não há necessidade de recriar o objeto construtor. Essa reutilização permite que o construtor atue como um modelo para criar muitos objetos imutáveis. É um pequeno benefício, mas poderia ser útil.
fonte
Na verdade, você pode ter os métodos do construtor em sua própria classe e ainda ter imutabilidade. Significa apenas que os métodos do construtor retornarão novos objetos, em vez de modificar o existente.
Isso funciona apenas se houver uma maneira de obter um objeto inicial (válido / útil) (por exemplo, de um construtor que defina todos os campos obrigatórios ou um método de fábrica que defina valores padrão), e os métodos adicionais do construtor retornem objetos modificados com base no existente. Esses métodos do construtor precisam garantir que você não receba objetos inválidos / inconsistentes no caminho.
Obviamente, isso significa que você terá muitos objetos novos e não deve fazer isso se seus objetos forem caros para criar.
Eu usei isso no código de teste para criar correspondências Hamcrest para um dos meus objetos de negócios. Não me lembro do código exato, mas parecia algo assim (simplificado):
Eu o usaria assim nos meus testes de unidade (com importações estáticas adequadas):
fonte
name
mas nãoage
), o conceito não funcionará. Bem, você pode realmente estar retornando algo comoPartiallyBuiltPerson
enquanto não é válido, mas parece um hack para mascarar o Builder.