Eu criei o que, para mim, é uma grande melhoria em relação ao Builder Pattern de Josh Bloch. Sem dizer que é "melhor", apenas que, em uma situação muito específica , fornece algumas vantagens - a maior delas é que desacopla o construtor de sua classe a ser construída.
Documentei completamente essa alternativa abaixo, que chamo de Padrão de construtor cego.
Padrão de Design: Construtor Cego
Como alternativa ao Builder Pattern de Joshua Bloch (item 2 em Java efetivo, 2ª edição), criei o que chamo de "Blind Builder Pattern", que compartilha muitos dos benefícios do Bloch Builder e, além de um único personagem, é usado exatamente da mesma maneira. Construtores cegos têm a vantagem de
- dissociando o construtor de sua classe anexa, eliminando uma dependência circular,
- reduz bastante o tamanho do código fonte (o que não é mais ) da classe envolvente e
- permite que a
ToBeBuilt
classe seja estendida sem precisar estender seu construtor .
Nesta documentação, vou me referir à classe que está sendo criada como " ToBeBuilt
" a classe.
Uma classe implementada com um Bloch Builder
Um Bloch Builder é um public static class
contido dentro da classe que ele cria. Um exemplo:
classe pública UserConfig {
private final String sName;
final privado int iAge;
final privado String sFavColor;
public UserConfig (UserConfig.Cfg uc_c) {// CONSTRUTOR
//transferir
experimentar {
sName = uc_c.sName;
} captura (NullPointerException rx) {
lança nova NullPointerException ("uc_c");
}
iAge = uc_c.iAge;
sFavColor = uc_c.sFavColor;
// VALIDAR TODOS OS CAMPOS AQUI
}
public String toString () {
retornar "nome =" + sName + ", idade =" + iAge + ", sFavColor =" + sFavColor;
}
//builder...START
classe estática pública Cfg {
private String sName;
private int iAge;
private String sFavColor;
Cfg público (String s_name) {
sName = s_name;
}
// setters com retorno automático ... START
idade pública do Cfg (int i_age) {
iAge = i_age;
devolva isso;
}
Cfg público favoriteColor (String s_color) {
sFavColor = s_color;
devolva isso;
}
// setters com retorno automático ... END
public UserConfig build () {
return (new UserConfig (this));
}
}
//builder...END
}
Instanciando uma classe com um Bloch Builder
UserConfig uc = new UserConfig.Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();
A mesma classe, implementada como um Blind Builder
Há três partes em um Blind Builder, cada uma delas em um arquivo de código-fonte separado:
- A
ToBeBuilt
classe (neste exemplo UserConfig
:)
- Sua
Fieldable
interface " "
- O construtor
1. A classe a ser construída
A classe a ser construída aceita sua Fieldable
interface como seu único parâmetro construtor. O construtor define todos os campos internos e valida cada um. Mais importante ainda, esta ToBeBuilt
classe não tem conhecimento de seu construtor.
classe pública UserConfig {
private final String sName;
final privado int iAge;
final privado String sFavColor;
public UserConfig (UserConfig_Fieldable uc_f) {// CONSTRUCTOR
//transferir
experimentar {
sName = uc_f.getName ();
} captura (NullPointerException rx) {
lança nova NullPointerException ("uc_f");
}
iAge = uc_f.getAge ();
sFavColor = uc_f.getFavoriteColor ();
// VALIDAR TODOS OS CAMPOS AQUI
}
public String toString () {
retornar "nome =" + sName + ", idade =" + iAge + ", sFavColor =" + sFavColor;
}
}
Conforme observado por um comentarista inteligente (que excluiu inexplicavelmente sua resposta), se a ToBeBuilt
classe também implementar sua Fieldable
, seu único construtor pode ser usado como construtor primário e de cópia (uma desvantagem é que os campos são sempre validados, mesmo que sabe-se que os campos no original ToBeBuilt
são válidos).
2. A Fieldable
interface " "
A interface de campo é a "ponte" entre a ToBeBuilt
classe e seu construtor, definindo todos os campos necessários para construir o objeto. Essa interface é requerida pelo ToBeBuilt
construtor de classes e é implementada pelo construtor. Como essa interface pode ser implementada por outras classes que não o construtor, qualquer classe pode instanciar facilmente a ToBeBuilt
classe, sem ser forçada a usar seu construtor. Isso também facilita a extensão da ToBeBuilt
classe, quando a extensão do construtor não é desejável ou necessária.
Conforme descrito na seção abaixo, eu não documento as funções nesta interface.
interface pública UserConfig_Fieldable {
String getName ();
int getAge ();
String getFavoriteColor ();
}
3. O construtor
O construtor implementa a Fieldable
classe. Ele não faz nenhuma validação e, para enfatizar esse fato, todos os seus campos são públicos e mutáveis. Embora essa acessibilidade pública não seja um requisito, prefiro e recomendo, porque reforça o fato de que a validação não ocorre até que o ToBeBuilt
construtor seja chamado. Isso é importante, porque é possível que outro encadeamento manipule ainda mais o construtor, antes de ser passado para o ToBeBuilt
construtor. A única maneira de garantir a validade dos campos - assumindo que o construtor não possa "bloquear" seu estado - é que a ToBeBuilt
classe faça a verificação final.
Finalmente, como na Fieldable
interface, não documento nenhum de seus getters.
classe pública UserConfig_Cfg implementa UserConfig_Fieldable {
public String sName;
public int iAge;
public String sFavColor;
public UserConfig_Cfg (String s_name) {
sName = s_name;
}
// setters com retorno automático ... START
idade pública de UserConfig_Cfg (int i_age) {
iAge = i_age;
devolva isso;
}
public UserConfig_Cfg favoriteColor (String s_color) {
sFavColor = s_color;
devolva isso;
}
// setters com retorno automático ... END
//getters...START
public String getName () {
return sName;
}
public int getAge () {
return iAge;
}
public String getFavoriteColor () {
return sFavColor;
}
//getters...END
public UserConfig build () {
return (new UserConfig (this));
}
}
Instanciando uma classe com um Blind Builder
UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();
A única diferença é " UserConfig_Cfg
" em vez de " UserConfig.Cfg
"
Notas
Desvantagens:
- Os cegos não podem acessar membros particulares de sua
ToBeBuilt
classe,
- Eles são mais detalhados, pois agora são necessários getters no construtor e na interface.
- Tudo para uma única aula não está mais em apenas um lugar .
Compilar um construtor cego é direto:
ToBeBuilt_Fieldable
ToBeBuilt
ToBeBuilt_Cfg
A Fieldable
interface é totalmente opcional
Para uma ToBeBuilt
classe com poucos campos obrigatórios - como esta UserConfig
classe de exemplo, o construtor pode simplesmente ser
public UserConfig (String s_name, int i_age, String s_favColor) {
E chamou o construtor com
public UserConfig build () {
return (novo UserConfig (getName (), getAge (), getFavoriteColor ()));
}
Ou mesmo eliminando completamente os getters (no construtor):
return (novo UserConfig (sName, iAge, sFavoriteColor));
Ao passar os campos diretamente, a ToBeBuilt
classe é tão "cega" (desconhece seu construtor) quanto na Fieldable
interface. No entanto, para ToBeBuilt
classes que e se destinam a ser "estendida e sub-estendido muitas vezes" (que está no título deste post), quaisquer alterações a quaisquer necessita de campo mudanças em todos os sub-classe, em cada construtor e ToBeBuilt
construtor. À medida que o número de campos e subclasses aumenta, torna-se impraticável de manter.
(De fato, com poucos campos necessários, o uso de um construtor pode ser um exagero. Para os interessados, aqui está uma amostra de algumas das interfaces Fieldable maiores na minha biblioteca pessoal.)
Classes secundárias no subpacote
Eu escolho ter todo o construtor e as Fieldable
classes, para todos os Construtores Cegos, em um subpacote de sua ToBeBuilt
classe. O subpacote é sempre chamado " z
". Isso evita que essas classes secundárias atravancem a lista de pacotes JavaDoc. Por exemplo
library.class.my.UserConfig
library.class.my.z.UserConfig_Fieldable
library.class.my.z.UserConfig_Cfg
Exemplo de validação
Como mencionado acima, toda validação ocorre no ToBeBuilt
construtor do. Aqui está o construtor novamente com o código de validação de exemplo:
public UserConfig (UserConfig_Fieldable uc_f) {
//transferir
experimentar {
sName = uc_f.getName ();
} captura (NullPointerException rx) {
lança nova NullPointerException ("uc_f");
}
iAge = uc_f.getAge ();
sFavColor = uc_f.getFavoriteColor ();
// validar (realmente deve pré-compilar os padrões ...)
experimentar {
if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
throw new IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") pode não estar vazio e deve conter apenas letras dígitos e sublinhados.");
}
} captura (NullPointerException rx) {
lança nova NullPointerException ("uc_f.getName ()");
}
if (iAge <0) {
throw new IllegalArgumentException ("uc_f.getAge () (" + iAge + ") é menor que zero.");
}
experimentar {
if (! Pattern.compile ("(?: vermelho | azul | verde | rosa quente)"). matcher (sFavColor) .matches ()) {
throw new IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") não é vermelho, azul, verde ou rosa quente.");
}
} captura (NullPointerException rx) {
lança nova NullPointerException ("uc_f.getFavoriteColor ()");
}
}
Documentando Construtores
Esta seção é aplicável aos Bloch Builders e Blind Builders. Ele demonstra como documento as classes neste design, fazendo com que os setters (no construtor) e seus getters (na ToBeBuilt
classe) sejam diretamente referenciados entre si - com um único clique do mouse e sem que o usuário precise saber onde essas funções realmente residem - e sem que o desenvolvedor precise documentar algo de forma redundante.
Getters: ToBeBuilt
Somente nas aulas
Os getters são documentados apenas na ToBeBuilt
classe. Os getters equivalentes nas classes _Fieldable
e
_Cfg
são ignorados. Eu não os documento.
/ **
<P> A idade do usuário. </P>
@return Um int representando a idade do usuário.
@see UserConfig_Cfg # age (int)
@see getName ()
** /
public int getAge () {
return iAge;
}
O primeiro @see
é um link para seu setter, que está na classe construtora.
Setters: Na classe de construtor
O setter está documentado como se está na ToBeBuilt
classe , e também como se ele faz a validação (que realmente é feito pelo ToBeBuilt
construtor 's). O asterisco (" *
") é uma pista visual que indica que o destino do link está em outra classe.
/ **
<P> Defina a idade do usuário. </P>
@param i_age Não pode ser inferior a zero. Entre com {@code UserConfig # getName () getName ()} *.
@see #favoriteColor (String)
** /
idade pública de UserConfig_Cfg (int i_age) {
iAge = i_age;
devolva isso;
}
Outras informações
Juntando tudo: a fonte completa do exemplo do Blind Builder, com documentação completa
UserConfig.java
importar java.util.regex.Pattern;
/ **
<P> Informações sobre um usuário - <I> [construtor: UserConfig_Cfg] </I> </P>
<P> A validação de todos os campos ocorre neste construtor de classes. No entanto, cada requisito de validação é um documento apenas nas funções de configuração do construtor. </P>
<P> {@code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </P>
** /
classe pública UserConfig {
público estático final void main (String [] igno_red) {
UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();
System.out.println (uc);
}
private final String sName;
final privado int iAge;
final privado String sFavColor;
/ **
<P> Crie uma nova instância. Isso define e valida todos os campos. </P>
@param uc_f Pode não ser {@code null}.
** /
public UserConfig (UserConfig_Fieldable uc_f) {
//transferir
experimentar {
sName = uc_f.getName ();
} captura (NullPointerException rx) {
lança nova NullPointerException ("uc_f");
}
iAge = uc_f.getAge ();
sFavColor = uc_f.getFavoriteColor ();
//validar
experimentar {
if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
throw new IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") pode não estar vazio e deve conter apenas letras dígitos e sublinhados.");
}
} captura (NullPointerException rx) {
lança nova NullPointerException ("uc_f.getName ()");
}
if (iAge <0) {
throw new IllegalArgumentException ("uc_f.getAge () (" + iAge + ") é menor que zero.");
}
experimentar {
if (! Pattern.compile ("(?: vermelho | azul | verde | rosa quente)"). matcher (sFavColor) .matches ()) {
throw new IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") não é vermelho, azul, verde ou rosa quente.");
}
} captura (NullPointerException rx) {
lança nova NullPointerException ("uc_f.getFavoriteColor ()");
}
}
//getters...START
/ **
<P> O nome do usuário. </P>
@return Uma string não - {@ code null}, não vazia.
@see UserConfig_Cfg # UserConfig_Cfg (String)
@see #getAge ()
@see #getFavoriteColor ()
** /
public String getName () {
return sName;
}
/ **
<P> A idade do usuário. </P>
@return Um número maior que ou igual a zero.
@see UserConfig_Cfg # age (int)
@see #getName ()
** /
public int getAge () {
return iAge;
}
/ **
<P> A cor favorita do usuário. </P>
@return Uma string não - {@ code null}, não vazia.
@see UserConfig_Cfg # age (int)
@see #getName ()
** /
public String getFavoriteColor () {
return sFavColor;
}
//getters...END
public String toString () {
retornar "getName () =" + getName () + ", getAge () =" + getAge () + ", getFavoriteColor () =" + getFavoriteColor ();
}
}
UserConfig_Fieldable.java
/ **
<P> Requerido pelo construtor {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable)}. </P>
** /
interface pública UserConfig_Fieldable {
String getName ();
int getAge ();
String getFavoriteColor ();
}
UserConfig_Cfg.java
importar java.util.regex.Pattern;
/ **
<P> Construtor para {@link UserConfig}. </P>
<P> A validação de todos os campos ocorre no construtor <CODE> UserConfig </CODE>. No entanto, cada requisito de validação é um documento apenas nessas funções do configurador de classes. </P>
** /
classe pública UserConfig_Cfg implementa UserConfig_Fieldable {
public String sName;
public int iAge;
public String sFavColor;
/ **
<P> Crie uma nova instância com o nome do usuário. </P>
@param s_name Não pode ser {@code null} ou vazio e deve conter apenas letras, dígitos e sublinhados. Conheça {@code UserConfig # getName () getName ()} {@ code ()} .
** /
public UserConfig_Cfg (String s_name) {
sName = s_name;
}
// setters com retorno automático ... START
/ **
<P> Defina a idade do usuário. </P>
@param i_age Não pode ser inferior a zero. Conheça {@code UserConfig # getName () getName ()} {@ code ()} .
@see #favoriteColor (String)
** /
idade pública de UserConfig_Cfg (int i_age) {
iAge = i_age;
devolva isso;
}
/ **
<P> Defina a cor favorita do usuário. </P>
@param s_color Deve ser {@code "red"}, {@code "blue"}, {@code green} ou {@code "hot pink"}. Conheça {@code UserConfig # getName () getName ()} {@ code ()} *.
@see #age (int)
** /
public UserConfig_Cfg favoriteColor (String s_color) {
sFavColor = s_color;
devolva isso;
}
// setters com retorno automático ... END
//getters...START
public String getName () {
return sName;
}
public int getAge () {
return iAge;
}
public String getFavoriteColor () {
return sFavColor;
}
//getters...END
/ **
<P> Crie o UserConfig, conforme configurado. </P>
@return <CODE> (novo {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (este)) </CODE>
** /
public UserConfig build () {
return (new UserConfig (this));
}
}
asImmutable
e inclua-o naReadableFoo
interface [usando essa filosofia, chamarbuild
um objeto imutável simplesmente retornaria uma referência ao mesmo objeto].*_Fieldable
e adicionar novos getters a ele, estender*_Cfg
e adicionar novos setters a ele, mas não vejo por que você precisaria reproduzir os getters e setters existentes. Eles são herdados e, a menos que precisem de funcionalidades diferentes, não há necessidade de recriá-los.Acho que a pergunta aqui assume algo desde o início, sem tentar provar que o padrão do construtor é inerentemente bom.
Acho que o padrão do construtor raramente é uma boa idéia.
Objetivo do Padrão do Construtor
O objetivo do padrão do construtor é manter duas regras que facilitarão o consumo de sua classe:
Os objetos não devem poder ser construídos em estados inconsistentes / inutilizáveis / inválidos.
Person
pode ser construídos objeto sem ter que éId
preenchido, enquanto todos os pedaços de código que uso esse objeto pode exigir oId
trabalho apenas para adequadamente com oPerson
.Os construtores de objetos não devem exigir muitos parâmetros .
Portanto, o objetivo do padrão do construtor não é controverso. Penso que grande parte do desejo e uso dele se baseia em análises que foram basicamente tão longe: queremos essas duas regras, isso dá essas duas regras - embora eu ache que vale a pena investigar outras maneiras de cumprir essas duas regras.
Por que se preocupar em olhar para outras abordagens?
Eu acho que a razão é bem demonstrada pelo fato desta questão em si; há complexidade e muita cerimônia adicionada às estruturas na aplicação do padrão do construtor a elas. Esta pergunta está perguntando como resolver parte dessa complexidade, porque, como costuma acontecer, ela cria um cenário que se comporta de maneira estranha (herdada). Essa complexidade também aumenta a sobrecarga de manutenção (adicionar, alterar ou remover propriedades é muito mais complexo do que o contrário).
Outras abordagens
Portanto, para a regra número um acima, que abordagens existem? A chave a que esta regra se refere é que, durante a construção, um objeto tem todas as informações necessárias para funcionar corretamente - e após a construção essas informações não podem ser alteradas externamente (portanto, são informações imutáveis).
Uma maneira de fornecer todas as informações necessárias a um objeto em construção é simplesmente adicionar parâmetros ao construtor. Se essas informações forem exigidas pelo construtor, você não poderá construir esse objeto sem todas essas informações, pois será construído em um estado válido. Mas e se o objeto exigir muita informação para ser válido? Oh caramba, se for esse o caso essa abordagem violaria a regra 2 acima .
Ok, o que mais há? Bem, você pode simplesmente pegar todas as informações necessárias para que seu objeto esteja em um estado consistente e agrupá-las em outro objeto que é obtido no momento da construção. Seu código acima, em vez de ter um padrão de construtor, seria:
Isso não é muito diferente do padrão do construtor, embora seja um pouco mais simples, e o mais importante é que estamos cumprindo a regra 1 e a regra 2 agora .
Então, por que não ir um pouco mais e torná-lo um construtor completo? É simplesmente desnecessário . Satisfizi ambos os propósitos do padrão do construtor nessa abordagem, com algo um pouco mais simples, mais fácil de manter e reutilizável . Esse último bit é a chave, este exemplo sendo usado é imaginário e não se presta ao objetivo semântico do mundo real, então vamos mostrar como essa abordagem resulta em um DTO reutilizável em vez de em uma classe de propósito único .
Portanto, quando você constrói DTOs coesos como este, eles podem satisfazer o propósito do padrão do construtor, de maneira mais simples e com maior valor / utilidade. Além disso, essa abordagem resolve a complexidade da herança que o padrão do construtor resulta em:
Você pode achar que o DTO nem sempre é coeso, ou para tornar os agrupamentos de propriedades coesos, eles precisam ser divididos em vários DTOs - isso não é realmente um problema. Se seu objeto exigir 18 propriedades e você puder criar 3 DTOs coesos com essas propriedades, você terá uma construção simples que atenda às finalidades dos construtores e mais algumas. Se você não conseguir criar agrupamentos coesos, isso pode ser um sinal de que seus objetos não são coesos se eles tiverem propriedades tão completamente não relacionadas - mas mesmo assim, a criação de um único DTO não coeso ainda é preferível devido à implementação mais simples. resolvendo seu problema de herança.
Como melhorar o padrão do construtor
Ok, então, todos os passeios de lado, você tem um problema e está procurando uma abordagem de design para resolvê-lo. Minha sugestão: herdar classes pode simplesmente ter uma classe aninhada que herda da classe construtora da superclasse, portanto a classe herdada tem basicamente a mesma estrutura que a superclasse e tem um padrão de construtor que deve funcionar exatamente da mesma forma com as funções adicionais para as propriedades adicionais da subclasse.
Quando é uma boa ideia
Afastando-se de lado, o padrão do construtor tem um nicho . Todos nós sabemos disso porque todos aprendemos esse construtor específico em um ponto ou outro:
StringBuilder
- aqui o objetivo não é uma construção simples, porque as seqüências de caracteres não poderiam ser mais fáceis de construir e concatenar etc. Esse é um ótimo construtor, pois possui um benefício de desempenho .O benefício de desempenho é assim: você tem um monte de objetos, eles são de um tipo imutável, é necessário reduzi-los a um objeto de um tipo imutável. Se você fizer isso de forma incremental, criará aqui muitos objetos intermediários, portanto, fazer tudo de uma só vez é muito mais eficiente e ideal.
Então, acho que a chave de quando é uma boa ideia está no domínio do problema do
StringBuilder
: Precisando transformar várias instâncias de tipos imutáveis em uma única instância de um tipo imutável .fonte
fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()
oferece uma API sucinta para a construção de foos e pode oferecer uma verificação de erro real no próprio construtor. Sem o construtor, o próprio objeto deve verificar suas entradas, o que significa que não estamos melhor do que costumávamos estar.Fieldable
parâmetro. Eu chamaria essa função de validação doToBeBuilt
construtor, mas poderia ser chamada por qualquer coisa, de qualquer lugar. Isso elimina o potencial de código redundante, sem forçar uma implementação específica. (E não há nada que o impeça de passar campos individuais para a função de validação, se você não gostar doFieldable
conceito - mas agora haveria pelo menos três lugares nos quais a lista de campos teria que ser mantida.)