Como melhorar o Padrão do Construtor de Bloch, para torná-lo mais apropriado para uso em classes altamente extensíveis

34

Fui bastante influenciado pelo livro Effective Java de Joshua Bloch (2ª edição), provavelmente mais do que com qualquer livro de programação que li. Em particular, seu Padrão de Construtor (item 2) teve o maior efeito.

Apesar do construtor de Bloch me levar muito mais longe nos últimos dez anos do que nos meus últimos dez anos de programação, ainda estou me encontrando na mesma parede: estender aulas com cadeias de métodos que retornam é, na melhor das hipóteses, desencorajador e, na pior das hipóteses, um pesadelo. --especialmente quando genéricos entram em jogo, e especialmente com os genéricos auto-referenciais (como Comparable<T extends Comparable<T>>).

Existem duas necessidades principais que tenho, apenas a segunda na qual gostaria de me concentrar nesta questão:

  1. O primeiro problema é "como compartilhar cadeias de métodos que retornam automaticamente, sem ter que reimplementá-las em todas as ... classes ...?" Para quem está curioso, eu abordei esta parte na parte inferior deste post de resposta, mas não é nisso que eu quero me concentrar aqui.

  2. O segundo problema, sobre o qual estou pedindo um comentário, é "como posso implementar um construtor em classes que se destinam a ser estendidas por muitas outras classes?" Estender uma classe com um construtor é naturalmente mais difícil do que estender uma classe sem. Estender uma classe que possui um construtor que também implementa Needablee, portanto, possui genéricos significativos associados a ela , é complicado.

Portanto, essa é a minha pergunta: como posso melhorar (o que chamo) de Bloch Builder, para me sentir à vontade para anexar um construtor a qualquer classe - mesmo quando essa classe for uma "classe base" que possa ser estendido e sub-estendido várias vezes - sem desencorajar o meu eu futuro, ou usuários da minha biblioteca , devido à bagagem extra que o construtor (e seus possíveis genéricos) lhes impõe?


Adendo
Minha pergunta se concentra na parte 2 acima, mas eu queria elaborar um pouco o problema um, incluindo como eu lidei com isso:

O primeiro problema é "como compartilhar cadeias de métodos que retornam automaticamente, sem ter que reimplementá-las em todas as ... classes ...?" Isso não impede que as classes estendidas tenham que reimplementar essas cadeias, o que, é claro, elas devem - em vez disso, como impedir que não subclasses , que desejam tirar vantagem dessas cadeias de métodos, precisem re-implementar -implementar todas as funções de retorno automático para que seus usuários possam tirar proveito delas? Para isso, criei um design necessário que imprimirei os esqueletos da interface aqui e deixo por enquanto. Funcionou bem para mim (esse projeto levou anos para ser produzido ... a parte mais difícil foi evitar dependências circulares):

public interface Chainable  {  
    Chainable chainID(boolean b_setStatic, Object o_id);  
    Object getChainID();  
    Object getStaticChainID();  
}
public interface Needable<O,R extends Needer> extends Chainable  {
    boolean isAvailableToNeeder();
    Needable<O,R> startConfigReturnNeedable(R n_eeder);
    R getActiveNeeder();
    boolean isNeededUsable();
    R endCfg();
}
public interface Needer  {
    void startConfig(Class<?> cls_needed);
    boolean isConfigActive();
    Class getNeededType();
    void neeadableSetsNeeded(Object o_fullyConfigured);
}
aliteralmind
fonte

Respostas:

21

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 ToBeBuiltclasse 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 classcontido 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:

  1. A ToBeBuiltclasse (neste exemplo UserConfig:)
  2. Sua Fieldableinterface " "
  3. O construtor

1. A classe a ser construída

A classe a ser construída aceita sua Fieldableinterface como seu único parâmetro construtor. O construtor define todos os campos internos e valida cada um. Mais importante ainda, esta ToBeBuiltclasse 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 ToBeBuiltclasse 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 ToBeBuiltsão válidos).

2. A Fieldableinterface " "

A interface de campo é a "ponte" entre a ToBeBuiltclasse e seu construtor, definindo todos os campos necessários para construir o objeto. Essa interface é requerida pelo ToBeBuiltconstrutor 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 ToBeBuiltclasse, sem ser forçada a usar seu construtor. Isso também facilita a extensão da ToBeBuiltclasse, 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 Fieldableclasse. 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 ToBeBuiltconstrutor seja chamado. Isso é importante, porque é possível que outro encadeamento manipule ainda mais o construtor, antes de ser passado para o ToBeBuiltconstrutor. A única maneira de garantir a validade dos campos - assumindo que o construtor não possa "bloquear" seu estado - é que a ToBeBuiltclasse faça a verificação final.

Finalmente, como na Fieldableinterface, 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 ToBeBuiltclasse,
  • 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:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

A Fieldableinterface é totalmente opcional

Para uma ToBeBuiltclasse com poucos campos obrigatórios - como esta UserConfigclasse 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 ToBeBuiltclasse é tão "cega" (desconhece seu construtor) quanto na Fieldableinterface. No entanto, para ToBeBuiltclasses 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 ToBeBuiltconstrutor. À 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 Fieldableclasses, para todos os Construtores Cegos, em um subpacote de sua ToBeBuiltclasse. 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 ToBeBuiltconstrutor 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 ToBeBuiltclasse) 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: ToBeBuiltSomente nas aulas

Os getters são documentados apenas na ToBeBuiltclasse. Os getters equivalentes nas classes _Fieldablee_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 ToBeBuiltclasse , e também como se ele faz a validação (que realmente é feito pelo ToBeBuiltconstrutor '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));
   }
}

aliteralmind
fonte
11
Definitivamente, é uma melhoria. O Bloch's Builder, conforme implementado aqui, combina duas classes concretas , sendo a futura a ser construída e o seu construtor. Esse é um design ruim por si só . O Blind Builder que você descreve quebra esse acoplamento ao ter a classe a ser construída, definir sua dependência de construção como uma abstração , que outras classes podem implementar de maneira dissociada. Você aplicou bastante o que é uma diretriz essencial de design orientado a objetos.
rucamzu
3
Você realmente deve escrever sobre isso em algum lugar, se ainda não o fez, um belo pedaço de design de algoritmo! Estou compartilhando agora :-).
Martijn Verburg
4
Obrigado pelas amáveis ​​palavras. Este é agora o primeiro post sobre o meu novo blog: aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind
Se o construtor e os objetos construídos implementarem Fieldable, o padrão começará a se parecer com um que eu chamei de ReadableFoo / MutableFoo / ImmutableFoo, embora em vez de ter o método para fazer uma coisa mutável ser o membro "build" do construtor, eu chame-o asImmutablee inclua-o na ReadableFoointerface [usando essa filosofia, chamar buildum objeto imutável simplesmente retornaria uma referência ao mesmo objeto].
Supercat
11
@ThomasN Você precisa estender *_Fieldablee adicionar novos getters a ele, estender *_Cfge 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.
aliteralmind
13

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:

  1. Os objetos não devem poder ser construídos em estados inconsistentes / inutilizáveis ​​/ inválidos.

    • Este refere-se a situações em que, por exemplo, um Personpode ser construídos objeto sem ter que é Idpreenchido, enquanto todos os pedaços de código que uso esse objeto pode exigir o Idtrabalho apenas para adequadamente com o Person.
  2. 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:

//DTO...START
public class Cfg  {
   public String sName    ;
   public int    iAge     ;
   public String sFavColor;
}
//DTO...END

public class UserConfig  {
   private final String sName    ;
   private final int    iAge     ;
   private final String sFavColor;
   public UserConfig(Cfg uc_c)  {
      ...
   }

   public String toString()  {
      return  "name=" + sName + ", age=" + iAge + ", sFavColor=" + sFavColor;
   }
}

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 .

public class NetworkAddress {
   public String Ip;
   public int Port;
   public NetworkAddress Proxy;
}

public class SocketConnection {
   public SocketConnection(NetworkAddress address) {
      ...
   }
}

public class FtpClient {
   public FtpClient(NetworkAddress address) {
      ...
   }
}

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:

public class SslCert {
   public NetworkAddress Authority;
   public byte[] PrivateKey;
   public byte[] PublicKey;
}

public class FtpsClient extends FtpClient {
   public FtpsClient(NetworkAddress address, SslCert cert) {
      super(address);
      ...
   }
}

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 .

Jimmy Hoffa
fonte
Não acho que o seu exemplo dado satisfaça nenhuma das regras. Não há nada que me impeça de criar um Cfg em um estado inválido, e enquanto os parâmetros foram movidos para fora do ctor, eles foram movidos para um local menos idiomático e mais detalhado. 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.
Phoshi
Os DTOs podem ter suas propriedades validadas de várias maneiras declarativamente com anotações, no setter, no entanto, você deseja fazer isso - a validação é um problema separado e, na abordagem do construtor, ele mostra a validação ocorrendo no construtor, a mesma lógica se encaixaria perfeitamente. na minha abordagem. No entanto, geralmente seria melhor usar o DTO para validá-lo, porque, como mostro - o DTO pode ser usado para construir vários tipos e, portanto, a validação se presta à validação de vários tipos. O construtor valida apenas para o tipo específico para o qual foi criado.
Jimmy Hoffa
Talvez a maneira mais flexível seja ter uma função de validação estática no construtor, que aceita um único Fieldableparâmetro. Eu chamaria essa função de validação do ToBeBuiltconstrutor, 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 do Fieldableconceito - mas agora haveria pelo menos três lugares nos quais a lista de campos teria que ser mantida.)
aliteralmind 17/02/2014
+1 E uma classe que tem muitas dependências em seu construtor obviamente não é coesa o suficiente e deve ser refatorada em classes menores.
Basilevs
@ JimmyHoffa: Ah, entendo, você acabou de omitir isso. Não tenho certeza de que vejo a diferença entre este e um construtor; depois disso, ele passa uma instância de configuração para o ctor em vez de chamar .build em algum construtor, e que um construtor tem um caminho mais óbvio para verificar a exatidão de todas os dados. Cada variável individual pode estar dentro de seus intervalos válidos, mas inválida nessa permutação específica. O .build pode verificar isso, mas passar o item para o ctor requer uma verificação de erros dentro do próprio objeto - nojento!
Phoshi