Come migliorare Bloch's Builder Pattern, per renderlo più appropriato per l'uso in classi altamente estensibili


34

Sono stato fortemente influenzato dal libro Effective Java di Joshua Bloch (2a edizione), probabilmente più che con qualsiasi libro di programmazione che ho letto. In particolare, il suo Builder Pattern (oggetto 2) ha avuto il massimo effetto.

Nonostante il costruttore di Bloch mi porti molto più lontano nel giro di un paio di mesi rispetto ai miei ultimi dieci anni di programmazione, mi trovo ancora a colpire lo stesso muro: estendere le classi con catene di metodi a ritorno automatico è nella migliore delle ipotesi scoraggiante, e nella peggiore dei casi un incubo - specialmente quando entrano in gioco i generici, e specialmente con i generici autoreferenziali (come Comparable<T extends Comparable<T>>).

Ci sono due bisogni principali che ho, solo il secondo su cui vorrei concentrarmi su questa domanda:

  1. Il primo problema è "come condividere catene di metodi a ritorno automatico, senza doverle implementare nuovamente in ogni ... singola ... classe?" Per coloro che potrebbero essere curiosi, ho affrontato questa parte in fondo a questo post di risposta, ma non è quello su cui voglio concentrarmi qui.

  2. Il secondo problema, su cui sto chiedendo un commento, è "come posso implementare un builder in classi che sono esse stesse destinate ad essere estese da molte altre classi?" Estendere una classe con un costruttore è naturalmente più difficile che estenderne una senza. L'estensione di una classe che ha anche un builder che implementa Needablee che quindi ha generici significativi associati è ingombrante.

Quindi questa è la mia domanda: come posso migliorare (quello che chiamo) il Bloch Builder, così posso sentirmi libero di associare un builder a qualsiasi classe - anche quando quella classe deve essere una "classe base" che può essere esteso e sub-esteso molte volte - senza scoraggiare il mio sé futuro, o gli utenti della mia biblioteca , a causa del bagaglio extra che il costruttore (e i suoi potenziali generici) impongono loro?


Addendum La
mia domanda si concentra sulla parte 2 sopra, ma volevo approfondire un po 'il problema, incluso il modo in cui l'ho affrontato:

Il primo problema è "come condividere catene di metodi a ritorno automatico, senza doverle implementare nuovamente in ogni ... singola ... classe?" Questo non impedisce alle classi estese di dover reimplementare queste catene, che, ovviamente, devono - piuttosto, come prevenire le non-sottoclassi , che vogliono trarre vantaggio da queste catene di metodi, dal dover -applicare ogni funzione di auto-ritorno in modo che i loro utenti possano trarne vantaggio? Per questo ho escogitato un disegno che necessita di bisogno che stamperò qui gli scheletri dell'interfaccia, e per ora lo lascerò a quello. Per me ha funzionato bene (questo progetto ha richiesto anni ... la parte più difficile è stata evitare le dipendenze circolari):

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

Risposte:


21

Ho creato quello che, per me, è un grande miglioramento rispetto al Builder Pattern di Josh Bloch. Per non dire in alcun modo che è "migliore", solo che in una situazione molto specifica , offre alcuni vantaggi - il più grande è che disaccoppia il costruttore dalla sua classe da costruire.

Ho ampiamente documentato questa alternativa di seguito, che chiamo Blind Builder Pattern.


Motivo di progettazione: Blind Builder

In alternativa a Joshua Bloch's Builder Pattern (elemento 2 in Effective Java, 2a edizione), ho creato quello che chiamo "Blind Builder Pattern", che condivide molti dei vantaggi di Bloch Builder e, a parte un singolo personaggio, è usato esattamente allo stesso modo. I costruttori ciechi hanno il vantaggio di

  • disaccoppiando il costruttore dalla sua classe chiusa, eliminando una dipendenza circolare,
  • riduce notevolmente la dimensione del codice sorgente di (ciò che non è più ) la classe che lo racchiude e
  • consente ToBeBuiltdi estendere la classe senza dover estendere il suo builder .

In questa documentazione, mi riferirò alla classe in costruzione come " ToBeBuilt" classe.

Una classe implementata con un Bloch Builder

Un Bloch Builder è un public static classcontenuto all'interno della classe che costruisce. Un esempio:

UserConfig di classe pubblica {
   stringa finale privata sName;
   int finale privato;
   String finale privata sFavColor;
   public UserConfig (UserConfig.Cfg uc_c) {// CONSTRUCTOR
      //trasferimento
         provare {
            sName = uc_c.sName;
         } catch (NullPointerException rx) {
            lancio nuovo NullPointerException ("uc_c");
         }
         iAge = uc_c.iAge;
         sFavColor = uc_c.sFavColor;
      // CONVALIDA QUI TUTTI I CAMPI
   }
   public String toString () {
      return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
   //builder...START
   classe statica pubblica Cfg {
      private String sName;
      private int iAge;
      String privata sFavColor;
      public Cfg (String s_name) {
         sName = s_name;
      }
      // setter con ritorno automatico ... INIZIA
         età Cfg pubblica (int i_age) {
            iAge = i_age;
            restituire questo;
         }
         public Cfg favoriteColor (String s_color) {
            sFavColor = s_color;
            restituire questo;
         }
      // setter con ritorno automatico ... END
      public UserConfig build () {
         return (nuovo UserConfig (this));
      }
   }
   //builder...END
}

Creare un'istanza di una classe con un Bloch Builder

UserConfig uc = new UserConfig.Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();

La stessa classe, implementata come Blind Builder

Ci sono tre parti in un Blind Builder, ognuna delle quali si trova in un file di codice sorgente separato:

  1. La ToBeBuiltclasse (in questo esempio UserConfig:)
  2. La sua " Fieldable" interfaccia
  3. Il costruttore

1. La classe da costruire

La classe da costruire accetta la sua Fieldableinterfaccia come unico parametro costruttore. Il costruttore imposta da esso tutti i campi interni e convalida ciascuno di essi. Ancora più importante, questa ToBeBuiltclasse non ha conoscenza del suo costruttore.

UserConfig di classe pubblica {
   stringa finale privata sName;
   int finale privato;
   String finale privata sFavColor;
    public UserConfig (UserConfig_Fieldable uc_f) {// CONSTRUCTOR
      //trasferimento
         provare {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            lancio nuovo NullPointerException ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // CONVALIDA QUI TUTTI I CAMPI
   }
   public String toString () {
      return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
}

Come notato da un commentatore intelligente (che inspiegabilmente cancellato la loro risposta), se la ToBeBuiltclasse implementa anche la sua Fieldable, il suo costruttore uno-e-solo può essere usato sia come suo primario e costruttore di copia (uno svantaggio è che i campi siano sempre convalidati, anche se è noto che i campi nell'originale ToBeBuiltsono validi).

2. L' Fieldableinterfaccia " "

L'interfaccia fieldable è il "ponte" tra la ToBeBuiltclasse e il suo builder, che definisce tutti i campi necessari per costruire l'oggetto. Questa interfaccia è richiesta dal ToBeBuiltcostruttore delle classi ed è implementata dal builder. Poiché questa interfaccia può essere implementata da classi diverse dal builder, qualsiasi classe può facilmente creare un'istanza della ToBeBuiltclasse, senza essere costretta a usare il suo builder. Questo rende anche più facile estendere la ToBeBuiltclasse, quando l'estensione del suo builder non è desiderabile o necessaria.

Come descritto in una sezione seguente, non documento affatto le funzioni di questa interfaccia.

interfaccia pubblica UserConfig_Fieldable {
   String getName ();
   int getAge ();
   String getFavoriteColor ();
}

3. Il costruttore

Il costruttore implementa la Fieldableclasse. Non convalida affatto e, per sottolineare questo fatto, tutti i suoi campi sono pubblici e mutevoli. Sebbene questa accessibilità pubblica non sia un requisito, la preferisco e la raccomando, poiché rafforza il fatto che la convalida non ha luogo finché non ToBeBuiltviene chiamato il costruttore. Questo è importante, perché è possibile che un altro thread manipoli ulteriormente il builder, prima che venga passato al ToBeBuiltcostruttore del. L'unico modo per garantire la validità dei campi - supponendo che il costruttore non possa in qualche modo "bloccare" il suo stato - è che la ToBeBuiltclasse esegua il controllo finale.

Infine, come con l' Fieldableinterfaccia, non documento nessuno dei suoi getter.

classe pubblica UserConfig_Cfg implementa UserConfig_Fieldable {
   public String sName;
   public int iAge;
    public String sFavColor;
    public UserConfig_Cfg (String s_name) {
       sName = s_name;
    }
    // setter con ritorno automatico ... INIZIA
       età UserConfig_Cfg pubblica (int i_age) {
          iAge = i_age;
          restituire questo;
       }
       publicConfig_Cfg favoriteColor (String s_color) {
          sFavColor = s_color;
          restituire questo;
       }
    // setter con ritorno automatico ... END
    //getters...START
       public String getName () {
          return sName;
       }
       public int getAge () {
          restituire iAge;
       }
       public String getFavoriteColor () {
          restituisce sFavColor;
       }
    //getters...END
    public UserConfig build () {
       return (nuovo UserConfig (this));
    }
}

Creare un'istanza di una classe con un Blind Builder

UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();

L'unica differenza è " UserConfig_Cfg" anziché " UserConfig.Cfg"

Gli appunti

svantaggi:

  • I Blind Builders non possono accedere ai membri privati ​​della sua ToBeBuiltclasse,
  • Sono più dettagliati, poiché i getter sono ora richiesti sia nel builder che nell'interfaccia.
  • Tutto per una singola classe non è più in un solo posto .

Compilare un Blind Builder è semplice:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

L' Fieldableinterfaccia è completamente opzionale

Per una ToBeBuiltclasse con pochi campi obbligatori - come questa UserConfigclasse di esempio, il costruttore potrebbe semplicemente essere

public UserConfig (String s_name, int i_age, String s_favColor) {

E chiamato il costruttore con

public UserConfig build () {
   return (new UserConfig (getName (), getAge (), getFavoriteColor ()));
}

O anche eliminando del tutto i getter (nel costruttore):

   return (nuovo UserConfig (sName, iAge, sFavoriteColor));

Passando direttamente i campi, la ToBeBuiltclasse è altrettanto "cieca" (ignara del suo builder) come lo è con l' Fieldableinterfaccia. Tuttavia, per le ToBeBuiltclassi che e devono essere "estese e sub-estese molte volte" (che si trova nel titolo di questo post), qualsiasi modifica a qualsiasi campo richiede modifiche in ogni sottoclasse, in ogni costruttore e ToBeBuiltcostruttore. All'aumentare del numero di campi e sottoclassi, ciò diventa impraticabile da mantenere.

(In effetti, con pochi campi necessari, l'uso di un builder potrebbe essere eccessivo. Per chi è interessato, ecco un campionamento di alcune delle più grandi interfacce Fieldable nella mia biblioteca personale.)

Classi secondarie nel sotto-pacchetto

Ho scelto di avere tutti i builder e le Fieldableclassi, per tutti i Blind Builders, in un sotto-pacchetto della loro ToBeBuiltclasse. Il sotto-pacchetto è sempre chiamato " z". Ciò impedisce a queste classi secondarie di ingombrare l'elenco dei pacchetti JavaDoc. Per esempio

  • library.class.my.UserConfig
  • library.class.my.z.UserConfig_Fieldable
  • library.class.my.z.UserConfig_Cfg

Esempio di convalida

Come accennato in precedenza, tutta la convalida si verifica nel ToBeBuiltcostruttore di. Ecco di nuovo il costruttore con un esempio di codice di convalida:

public UserConfig (UserConfig_Fieldable uc_f) {
   //trasferimento
      provare {
         sName = uc_f.getName ();
      } catch (NullPointerException rx) {
         lancio nuovo NullPointerException ("uc_f");
      }
      iAge = uc_f.getAge ();
      sFavColor = uc_f.getFavoriteColor ();
   // validate (dovrebbe davvero pre-compilare i pattern ...)
      provare {
         if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
            throw new IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") potrebbe non essere vuoto e deve contenere solo lettere, cifre e caratteri di sottolineatura.");
         }
      } catch (NullPointerException rx) {
         throw new NullPointerException ("uc_f.getName ()");
      }
      if (iAge <0) {
         throw new IllegalArgumentException ("uc_f.getAge () (" + iAge + ") è inferiore a zero.");
      }
      provare {
         if (! Pattern.compile ("(?: red | blue | green | hot pink)"). matcher (sFavColor) .matches ()) {
            lanciare nuovo IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") non è rosso, blu, verde o rosa caldo.");
         }
      } catch (NullPointerException rx) {
         lancia nuovo NullPointerException ("uc_f.getFavoriteColor ()");
      }
}

Costruttori di documentazione

Questa sezione è applicabile sia a Bloch Builders che a Blind Builders. Dimostra come documentare le classi in questo progetto, creando setter (nel costruttore) e loro getter (nella ToBeBuiltclasse) con riferimenti incrociati direttamente tra loro - con un solo clic del mouse e senza che l'utente debba sapere dove tali funzioni risiedono effettivamente e senza che lo sviluppatore debba documentare nulla in modo ridondante.

Getters: ToBeBuiltsolo nelle classi

I getter sono documentati solo nella ToBeBuiltclasse. I getter equivalenti sia nelle classi _Fieldableche nelle_Cfg classi vengono ignorati. Non li documento affatto.

/ **
   <P> L'età dell'utente. </P>
   @return Un int che rappresenta l'età dell'utente.
   @vedi UserConfig_Cfg # age (int)
   @vedi getName ()
 ** /
public int getAge () {
   restituire iAge;
}

Il primo @seeè un link al suo setter, che è nella classe builder.

Incastonatori: nella classe costruttore

Il setter è documentato come se fosse in ToBeBuiltclasse , e anche come se si fa la convalida (che in realtà è fatto da parte del ToBeBuilt's costruttore). L'asterisco (" *") è un indizio visivo che indica che la destinazione del collegamento è in un'altra classe.

/ **
   <P> Imposta l'età dell'utente. </P>
   @param i_age Potrebbe non essere inferiore a zero. Ottieni con {@code UserConfig # getName () getName ()} *.
   @see #favoriteColor (String)
 ** /
età UserConfig_Cfg pubblica (int i_age) {
   iAge = i_age;
   restituire questo;
}

Ulteriori informazioni

Mettere tutto insieme: la fonte completa dell'esempio di Blind Builder, con documentazione completa

UserConfig.java

import java.util.regex.Pattern;
/ **
   <P> Informazioni su un utente - <I> [builder: UserConfig_Cfg] </I> </P>
   <P> La convalida di tutti i campi si verifica in questo costruttore di classi. Tuttavia, ogni requisito di convalida è un documento solo nelle funzioni setter del builder. </P>
   <P> {@code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </P>
 ** /
UserConfig di classe pubblica {
   public static final void main (String [] igno_red) {
      UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();
      System.out.println (UC);
   }
   stringa finale privata sName;
   int finale privato;
   String finale privata sFavColor;
   / **
      <P> Crea una nuova istanza. Questo imposta e convalida tutti i campi. </P>
      @param uc_f Potrebbe non essere {@code null}.
    ** /
   public UserConfig (UserConfig_Fieldable uc_f) {
      //trasferimento
         provare {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            lancio nuovo NullPointerException ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      //convalidare
         provare {
            if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
               throw new IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") potrebbe non essere vuoto e deve contenere solo lettere, cifre e caratteri di sottolineatura.");
            }
         } catch (NullPointerException rx) {
            throw new NullPointerException ("uc_f.getName ()");
         }
         if (iAge <0) {
            throw new IllegalArgumentException ("uc_f.getAge () (" + iAge + ") è inferiore a zero.");
         }
         provare {
            if (! Pattern.compile ("(?: red | blue | green | hot pink)"). matcher (sFavColor) .matches ()) {
               lanciare nuovo IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") non è rosso, blu, verde o rosa caldo.");
            }
         } catch (NullPointerException rx) {
            lancia nuovo NullPointerException ("uc_f.getFavoriteColor ()");
         }
   }
   //getters...START
      / **
         <P> Il nome dell'utente. </P>
         @return Una stringa non - {@ code null}, non vuota.
         @see UserConfig_Cfg # UserConfig_Cfg (String)
         @see #getAge ()
         @see #getFavoriteColor ()
       ** /
      public String getName () {
         return sName;
      }
      / **
         <P> L'età dell'utente. </P>
         @return Un numero maggiore o uguale a zero.
         @vedi UserConfig_Cfg # age (int)
         @vedi #getName ()
       ** /
      public int getAge () {
         restituire iAge;
      }
      / **
         <P> Il colore preferito dell'utente. </P>
         @return Una stringa non - {@ code null}, non vuota.
         @vedi UserConfig_Cfg # age (int)
         @vedi #getName ()
       ** /
      public String getFavoriteColor () {
         restituisce sFavColor;
      }
   //getters...END
   public String toString () {
      return "getName () =" + getName () + ", getAge () =" + getAge () + ", getFavoriteColor () =" + getFavoriteColor ();
   }
}

UserConfig_Fieldable.java

/ **
   <P> Richiesto dal costruttore {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable)}. </P>
 ** /
interfaccia pubblica UserConfig_Fieldable {
   String getName ();
   int getAge ();
   String getFavoriteColor ();
}

UserConfig_Cfg.java

import java.util.regex.Pattern;
/ **
   <P> Builder per {@link UserConfig}. </P>
   <P> La convalida di tutti i campi si verifica nel costruttore <CODE> UserConfig </CODE>. Tuttavia, ogni requisito di convalida è documentato solo nelle funzioni di setter di questa classe. </P>
 ** /
classe pubblica UserConfig_Cfg implementa UserConfig_Fieldable {
   public String sName;
   public int iAge;
   public String sFavColor;
   / **
      <P> Crea una nuova istanza con il nome dell'utente. </P>
      @param s_name Non può essere {@code null} o vuoto e deve contenere solo lettere, cifre e caratteri di sottolineatura. Ottieni con {@code UserConfig # getName () getName ()} {@ code ()} .
    ** /
   public UserConfig_Cfg (String s_name) {
      sName = s_name;
   }
   // setter con ritorno automatico ... INIZIA
      / **
         <P> Imposta l'età dell'utente. </P>
         @param i_age Potrebbe non essere inferiore a zero. Ottieni con {@code UserConfig # getName () getName ()} {@ code ()} .
         @see #favoriteColor (String)
       ** /
      età UserConfig_Cfg pubblica (int i_age) {
         iAge = i_age;
         restituire questo;
      }
      / **
         <P> Imposta il colore preferito dell'utente. </P>
         @param s_color Deve essere {@code "red"}, {@code "blue"}, {@code green} o {@code "hot pink"}. Ottieni con {@code UserConfig # getName () getName ()} {@ code ()} *.
         @see #age (int)
       ** /
      publicConfig_Cfg favoriteColor (String s_color) {
         sFavColor = s_color;
         restituire questo;
      }
   // setter con ritorno automatico ... END
   //getters...START
      public String getName () {
         return sName;
      }
      public int getAge () {
         restituire iAge;
      }
      public String getFavoriteColor () {
         restituisce sFavColor;
      }
   //getters...END
   / **
      <P> Crea UserConfig, come configurato. </P>
      @return <CODICE> (nuovo {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (questo)) </CODE>
    ** /
   public UserConfig build () {
      return (nuovo UserConfig (this));
   }
}


1
Sicuramente, è un miglioramento. The Bloch's Builder, come implementato qui, accoppia due classi concrete , queste sono quelle da costruire e il suo costruttore. Questo è un cattivo design di per sé . Il Blind Builder che descrivi interrompe quell'accoppiamento facendo in modo che la classe da costruire definisca la sua dipendenza da costruzione come un'astrazione , che altre classi possono implementare in modo disaccoppiato. Hai ampiamente applicato ciò che è una linea guida di progettazione orientata agli oggetti essenziale.
Rucamzu,

3
Dovresti davvero blog su questo da qualche parte se non l'hai già fatto, bel pezzo di progettazione di algoritmi! Ora sto condividendo :-).
Martijn Verburg,

4
Grazie per le gentili parole. Questo è ora il primo post sul mio nuovo blog: aliteralmind.wordpress.com/2014/02/02/14/blind_builder
aliteralmind

Se il builder e gli oggetti creati implementano entrambi Fieldable, il modello inizia ad assomigliare a quello che ho definito ReadableFoo / MutableFoo / ImmutableFoo, sebbene invece di avere il metodo per rendere una cosa mutevole sia il membro "build" del builder, I chiamalo asImmutablee includilo ReadableFoonell'interfaccia [usando quella filosofia, invocare buildun oggetto immutabile restituirebbe semplicemente un riferimento allo stesso oggetto].
supercat

1
@ThomasN È necessario estendere *_Fieldablee aggiungere nuovi getter, estendere *_Cfge aggiungere nuovi setter, ma non vedo perché dovresti riprodurre getter e setter esistenti. Sono ereditati e, a meno che non necessitino di funzionalità diverse, non è necessario ricrearli.
aliteralmind,

13

Penso che la domanda qui presuma qualcosa dall'inizio, senza tentare di dimostrarlo, che il modello del costruttore è intrinsecamente buono.

tl; dr Penso che il modello del costruttore sia raramente una buona idea.


Scopo del modello del costruttore

Lo scopo del modello builder è mantenere due regole che renderanno più semplice il consumo della classe:

  1. Gli oggetti non dovrebbero poter essere costruiti in stati incoerenti / inutilizzabili / non validi.

    • Questo si riferisce a scenari in cui, ad esempio, un Personoggetto può essere costruito senza averlo Idcompilato, mentre tutti i pezzi di codice che usano quell'oggetto possono richiedere al Idgiusto di funzionare correttamente con Person.
  2. I costruttori di oggetti non dovrebbero richiedere troppi parametri .

Quindi lo scopo del modello del costruttore non è controverso. Penso che gran parte del desiderio e del suo utilizzo si basino su analisi che sono andate sostanzialmente così lontano: vogliamo queste due regole, questo ci dà queste due regole - anche se penso che valga la pena investigare altri modi per raggiungere quelle due regole.


Perché preoccuparsi di guardare altri approcci?

Penso che la ragione sia ben dimostrata dal fatto stesso di questa domanda; c'è complessità e molta cerimonia aggiunta alle strutture nell'applicare il modello di costruzione a loro. Questa domanda è come risolvere una parte di quella complessità perché, come spesso accade nella complessità, crea uno scenario che si comporta in modo strano (ereditando). Questa complessità aumenta anche i costi di manutenzione (l'aggiunta, la modifica o la rimozione di proprietà è molto più complessa di quanto non faccia).


Altri approcci

Quindi, per la regola numero uno sopra, quali sono gli approcci? La chiave a cui si riferisce questa regola è che durante la costruzione, un oggetto ha tutte le informazioni necessarie per funzionare correttamente - e dopo la costruzione tali informazioni non possono essere modificate esternamente (quindi sono informazioni immutabili).

Un modo per fornire tutte le informazioni necessarie a un oggetto in fase di costruzione è semplicemente aggiungere parametri al costruttore. Se tali informazioni sono richieste dal costruttore, non sarai in grado di costruire questo oggetto senza tutte quelle informazioni, quindi sarà costruito in uno stato valido. Ma cosa succede se l'oggetto richiede molte informazioni per essere valido? Oh dannazione, se fosse così questo approccio avrebbe infranto la regola 2 sopra .

Ok cos'altro c'è? Bene, potresti semplicemente prendere tutte le informazioni necessarie affinché il tuo oggetto sia in uno stato coerente e raggrupparlo in un altro oggetto che viene preso in fase di costruzione. Il tuo codice sopra invece di avere un modello builder sarebbe quindi:

//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;
   }
}

Questo non è molto diverso dal modello del costruttore, anche se è leggermente più semplice, e soprattutto stiamo soddisfacendo la regola n. 1 e la regola n. 2 ora .

Quindi perché non andare un po 'di più e renderlo un costruttore pieno? È semplicemente superfluo . Ho soddisfatto entrambi gli scopi del modello del costruttore in questo approccio, con qualcosa di leggermente più semplice, più facile da mantenere e riutilizzabile . L'ultimo bit è fondamentale, questo esempio utilizzato è immaginario e non si presta allo scopo semantico del mondo reale, quindi mostriamo come questo approccio si traduca in un DTO riutilizzabile piuttosto che in una singola classe di scopo .

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) {
      ...
   }
}

Quindi quando costruisci coesivo DTO come questo, entrambi possono soddisfare lo scopo del modello del costruttore, in modo più semplice e con un valore / utilità più ampi. Inoltre, questo approccio risolve la complessità dell'ereditarietà che il modello builder comporta:

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);
      ...
   }
}

Potresti scoprire che il DTO non è sempre coerente, o per rendere coerenti i raggruppamenti di proprietà devono essere suddivisi su più DTO - questo non è davvero un problema. Se il tuo oggetto richiede 18 proprietà e puoi realizzare 3 DTO coerenti con quelle proprietà, hai una costruzione semplice che soddisfa gli scopi dei costruttori, e poi alcune. Se non riesci a creare raggruppamenti coesivi, questo potrebbe essere un segno che i tuoi oggetti non sono coesivi se hanno proprietà che sono così completamente non correlate - ma anche in questo caso è preferibile realizzare un singolo DTO non coesivo a causa dell'implementazione più semplice e risolvere il tuo problema di eredità.


Come migliorare il modello del costruttore

Ok, quindi, a parte tutte le chiacchiere rimble, hai un problema e stai cercando un approccio progettuale per risolverlo. Il mio consiglio: ereditare le classi può semplicemente avere una classe nidificata che eredita dalla classe builder della superclasse, quindi la classe ereditaria ha sostanzialmente la stessa struttura della superclasse e ha un modello builder che dovrebbe funzionare esattamente allo stesso modo con le funzioni aggiuntive per le proprietà aggiuntive della sottoclasse.


Quando è una buona idea

A parte questo, il modello del costruttore ha una nicchia . Lo sappiamo tutti perché abbiamo imparato questo particolare costruttore in un punto o nell'altro:StringBuilder - qui lo scopo non è semplice costruzione, perché le stringhe non potrebbero essere più facili da costruire e concatenare ecc. Questo è un grande costruttore perché ha un vantaggio in termini di prestazioni .

Il vantaggio in termini di prestazioni è quindi: hai un sacco di oggetti, sono di tipo immutabile, devi comprimerli fino a un oggetto di tipo immutabile. Se lo fai in modo incrementale, qui creerai molti oggetti intermedi, quindi farlo tutto in una volta è molto più performante e ideale.

Quindi penso che la chiave di quando è una buona idea sia nel dominio problematico di StringBuilder: La necessità di trasformare più istanze di tipi immutabili in una singola istanza di un tipo immutabile .


Non credo che il tuo esempio dato soddisfi nessuna delle due regole. Non c'è niente che mi impedisce di creare un Cfg in uno stato non valido, e mentre i parametri sono stati spostati fuori dal ctor, sono stati appena spostati in un posto meno idiomatico e più prolisso. fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()offre un'API sintetica per la creazione di foos e può offrire un controllo degli errori effettivo nel builder stesso. Senza il costruttore l'oggetto stesso deve controllare i suoi input, il che significa che non stiamo meglio di prima.
Phoshi,

I DTO possono convalidare le loro proprietà in numerosi modi in modo dichiarativo con annotazioni, sul setter, comunque tu voglia andare su di esso - la convalida è un problema separato e nel suo approccio builder mostra la convalida che si verifica nel costruttore, che la stessa logica si adatterebbe perfettamente bene nel mio approccio. Tuttavia, sarebbe generalmente meglio usare il DTO per farlo convalidare perché, come mostro, il DTO può essere usato per costruire più tipi e quindi avere la convalida su di esso si presterebbe alla convalida di più tipi. Il builder convalida solo per un tipo particolare per cui è stato creato.
Jimmy Hoffa,

Forse il modo più flessibile sarebbe avere una funzione di validazione statica nel builder, che accetta un singolo Fieldableparametro. Io chiamerei questa funzione convalida dal ToBeBuiltcostruttore, ma potrebbe essere chiamato da qualsiasi cosa, da qualsiasi luogo. Ciò elimina il potenziale di codice ridondante, senza forzare un'implementazione specifica. (E non c'è nulla che ti impedisca di passare nei singoli campi alla funzione di validazione, se non ti piace il Fieldableconcetto - ma ora ci sarebbero almeno tre posti in cui l'elenco dei campi dovrebbe essere mantenuto.)
aliteralmind,

+1 E una classe che ha troppe dipendenze nel suo costruttore non è ovviamente abbastanza coerente e dovrebbe essere rifattorizzata in classi più piccole.
Basilevs,

@JimmyHoffa: Ah, vedo, l'hai omesso. Non sono sicuro di vedere la differenza tra questo e un builder, quindi, a parte questo, passa un'istanza di configurazione nel ctor invece di chiamare .build su qualche builder e che un builder ha un percorso più ovvio per il controllo di correttezza su tutto i dati. Ogni singola variabile potrebbe essere all'interno dei suoi intervalli validi, ma non valida in quella particolare permutazione. .build può verificarlo, ma passare l'oggetto nel ctor richiede un controllo degli errori all'interno dell'oggetto stesso - icky!
Foshi,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.