it-swarm.com.de

Wie man das Builder-Muster von Bloch verbessert, um es für die Verwendung in hoch erweiterbaren Klassen besser geeignet zu machen

Ich wurde stark von Joshua Blochs Effective Java Buch (2. Auflage)) beeinflusst, wahrscheinlich mehr als bei jedem anderen Programmierbuch, das ich gelesen habe. Insbesondere sein Builder Pattern (Punkt 2) hat es getan der größte Effekt.

Obwohl Blochs Erbauer mich in den paar Monaten viel weiter gebracht hat als in den letzten zehn Jahren des Programmierens, stoße ich immer noch auf die gleiche Wand: Das Erweitern von Klassen mit sich selbst zurückkehrenden Methodenketten ist bestenfalls entmutigend und im schlimmsten Fall ein Albtraum - besonders wenn Generika ins Spiel kommen und besonders mit selbstreferenzielle Generika (wie Comparable<T extends Comparable<T>>).

Ich habe zwei Hauptbedürfnisse, von denen ich mich in dieser Frage nur auf das zweite konzentrieren möchte:

  1. Das erste Problem lautet: "Wie können sich selbst zurückgebende Methodenketten gemeinsam genutzt werden, ohne dass sie in jeder ... einzelnen ... Klasse erneut implementiert werden müssen?" Für diejenigen, die vielleicht neugierig sind, habe ich diesen Teil am Ende dieses Antwortposts angesprochen, aber darauf möchte ich mich hier nicht konzentrieren.

  2. Das zweite Problem, zu dem ich um einen Kommentar bitte, lautet: "Wie kann ich einen Builder in Klassen implementieren, die selbst um viele andere Klassen erweitert werden sollen?" Das Erweitern einer Klasse mit einem Builder ist natürlich schwieriger als das Erweitern einer Klasse ohne. Das Erweitern einer Klasse mit einem Builder , der auch Needable implementiert und daher mit signifikanten Generika verknüpft ist, ist unhandlich.

Das ist also meine Frage: Wie kann ich den Bloch Builder verbessern (was ich nenne), damit ich einen Builder an die Klasse any anhängen kann - auch wenn diese Klasse dazu bestimmt ist eine "Basisklasse" sein, die um ein Vielfaches erweitert und erweitert werden kann - ohne mein zukünftiges Selbst oder Benutzer meiner Bibliothek zu entmutigen, aufgrund des zusätzlichen Gepäcks des Erbauers (und ihre potenziellen Generika) ihnen auferlegen?


Nachtrag
Meine Frage konzentriert sich auf Teil 2 oben, aber ich wollte ein wenig auf das erste Problem eingehen, einschließlich der Art und Weise, wie ich damit umgegangen bin:

Das erste Problem lautet: "Wie können sich selbst zurückgebende Methodenketten gemeinsam genutzt werden, ohne dass sie in jeder ... einzelnen ... Klasse erneut implementiert werden müssen?" Dies soll nicht verhindern, dass Klassen erweitern Diese Ketten erneut implementieren müssen, was sie natürlich müssen - vielmehr, wie Nicht-Unterklassen zu verhindern sind, die diese Methodenketten nutzen möchten, indem sie jede selbstrückgebende Funktion erneut implementieren müssen, damit ihre Benutzer sie nutzen können? Dafür habe ich mir ein bedarfsgerechtes Design ausgedacht, für das ich die Schnittstellenskelette hier ausdrucken und vorerst belassen werde. Es hat gut für mich funktioniert (dieses Design war Jahre in der Herstellung ... das Schwierigste war, kreisförmige Abhängigkeiten zu vermeiden):

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

Ich habe eine für mich große Verbesserung gegenüber Josh Blochs Builder-Muster geschaffen. Um nicht zu sagen, dass es "besser" ist, nur dass es in einer sehr spezifischen Situation einige Vorteile bietet - der größte ist der es entkoppelt den Builder von seiner zu bauenden Klasse.

Ich habe diese Alternative, die ich Blind Builder Pattern nenne, unten gründlich dokumentiert.


Entwurfsmuster: Blind Builder

Als Alternative zu Joshua Blochs Builder Pattern (Punkt 2 in Effective Java, 2. Ausgabe) habe ich das sogenannte "Blind Builder Pattern" erstellt, das viele der Vorteile von Bloch Builder und wird, abgesehen von einem einzelnen Zeichen, genauso verwendet. Blinde Bauherren haben den Vorteil von

  • entkoppeln des Builders von seiner umschließenden Klasse, Eliminieren einer zirkulären Abhängigkeit,
  • reduziert die Größe des Quellcodes von (was ist) stark nicht mehr, nicht länger) die einschließende Klasse und
  • ermöglicht die Erweiterung der Klasse ToBeBuilt ohne seinen Erbauer erweitern zu müssen.

In dieser Dokumentation bezeichne ich die Klasse, die erstellt wird, als "ToBeBuilt" -Klasse.

Eine Klasse, die mit einem Bloch Builder implementiert wurde

Ein Bloch Builder ist ein public static class in der Klasse enthalten, die es erstellt. Ein Beispiel:

öffentliche Klasse UserConfig {
 private final String sName; 
 private final int iAge; 
 private final String sFavColor; 
 public UserConfig (UserConfig.Cfg uc_c) {// CONSTRUCTOR 
 //Transfer
 Try {
 SName = uc_c.sName; 
} Catch (NullPointerException rx) {
 Wirf eine neue NullPointerException ("uc_c "); 
} 
 iAge = uc_c.iAge; 
 sFavColor = uc_c.sFavColor; 
 // ALLE FELDER HIER VALIDIEREN 
} 
 public String toString () {
 return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor; 
} 
//builder...START
 öffentliche statische Klasse Cfg {
 private Zeichenfolge sName; 
 private int iAge; 
 private Zeichenfolge sFavColor; 
 öffentliche Cfg (String s_name) {
 SName = s_name; 
} 
 // Se Wenn Setter zurückkehren ... START 
 public Cfg age (int i_age) {
 iAge = i_age; 
 return this; 
} 
 public Cfg favorColor (String s_color) {
 SFavColor = s_color; 
 Gibt dies zurück; 
} 
 // Selbstrückgebende Setter ... END 
 public UserConfig build () {
 return (neue UserConfig (this)); 
} 
} 
 //builder...END
}

Instanziieren einer Klasse mit einem Bloch Builder

UserConfig uc = new UserConfig.Cfg ("Kermit"). Alter (50) .favoriteColor ("grün"). Build (); 

Dieselbe Klasse, implementiert als Blind Builder

Ein Blind Builder besteht aus drei Teilen, die sich jeweils in einer separaten Quellcodedatei befinden:

  1. Die Klasse ToBeBuilt (in diesem Beispiel: UserConfig)
  2. Die Schnittstelle "Fieldable"
  3. Der Bauarbeiter

1. Die zu bauende Klasse

Die zu erstellende Klasse akzeptiert ihre Fieldable -Schnittstelle als einzigen Konstruktorparameter. Der Konstruktor legt alle internen Felder daraus fest. und validiert jeder. Am wichtigsten ist, dass diese ToBeBuilt -Klasse keine Kenntnisse über ihren Builder hat.

öffentliche Klasse UserConfig {
 private final String sName; 
 private final int iAge; 
 private final String sFavColor; 
 public UserConfig (UserConfig_Fieldable uc_f) {// CONSTRUCTOR 
 //transfer
 try {
 sName = uc_f.getName (); 
} catch (NullPointerException rx) {
 throw new NullPointerException ("uc_f "); 
} 
 iAge = uc_f.getAge (); 
 sFavColor = uc_f.getFavoriteColor (); 
 // ALLE FELDER HIER VALIDIEREN 
} 
 public String toString () {
 return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor; 
} 
}

Wie von einem intelligenten Kommentator (der unerklärlicherweise seine Antwort gelöscht hat) festgestellt wurde, kann, wenn die Klasse ToBeBuilt auch ihre Fieldable implementiert, ihr einziger Konstruktor als primärer Konstruktor verwendet werden und Kopierkonstruktor (ein Nachteil ist, dass Felder immer validiert werden, obwohl bekannt ist, dass die Felder im ursprünglichen ToBeBuilt gültig sind).

2. Die Schnittstelle "Fieldable"

Die feldfähige Schnittstelle ist die "Brücke" zwischen der Klasse ToBeBuilt und ihrem Builder und definiert alle Felder, die zum Erstellen des Objekts erforderlich sind. Diese Schnittstelle wird vom Konstruktor ToBeBuilt classes benötigt und vom Builder implementiert. Da diese Schnittstelle von anderen Klassen als dem Builder implementiert werden kann, kann jede Klasse die Klasse ToBeBuilt leicht instanziieren, ohne gezwungen zu sein, ihren Builder zu verwenden. Dies erleichtert auch das Erweitern der Klasse ToBeBuilt, wenn das Erweitern des Builders nicht wünschenswert oder erforderlich ist.

Wie in einem der folgenden Abschnitte beschrieben, dokumentiere ich die Funktionen in dieser Schnittstelle überhaupt nicht.

öffentliche Schnittstelle UserConfig_Fieldable {
 String getName (); 
 int getAge (); 
 String getFavoriteColor (); 
}

3. Der Erbauer

Der Builder implementiert die Klasse Fieldable. Es gibt überhaupt keine Validierung, und um diese Tatsache zu betonen, sind alle seine Bereiche öffentlich und veränderlich. Obwohl diese öffentliche Zugänglichkeit keine Voraussetzung ist, bevorzuge und empfehle ich sie, da sie die Tatsache verstärkt, dass die Validierung erst erfolgt, wenn der Konstruktor des ToBeBuilt aufgerufen wird. Das ist wichtig, weil es so ist möglich Damit ein anderer Thread den Builder weiter manipuliert, bevor er an den Konstruktor des ToBeBuilt übergeben wird. Die einzige Möglichkeit, die Gültigkeit der Felder zu gewährleisten - vorausgesetzt, der Builder kann seinen Status nicht irgendwie "sperren" - besteht darin, dass die Klasse ToBeBuilt die Endprüfung durchführt.

Schließlich dokumentiere ich, wie bei der Fieldable -Schnittstelle, keinen ihrer Getter.

public class UserConfig_Cfg implementiert UserConfig_Fieldable {
 public String sName; 
 public int iAge; 
 public String sFavColor; 
 public UserConfig_Cfg (String s_name) {
 sName = s_name; 
} 
 // selbstgebende Setter ... START 
 public UserConfig_Cfg age (int i_age) {
 iAge = i_age; 
 return this; 
} 
 public UserConfig_Cfg favorColor (String s_color) {
 sFavColor = s_color; 
 return this; 
} 
 // selbstrückgebende Setter ... END 
 //getters...START
 public String getName () {
 return sName; 
} 
 public int getAge () {
 return iAge; 
} 
 public String getFavoriteColor () {
 return sFavColor; 
} 
 //getters...END
 public UserConfig build () {
 return (neue UserConfig (this)); 
} 
}

Instanziieren einer Klasse mit einem Blind Builder

UserConfig uc = new UserConfig_Cfg ("Kermit"). Alter (50) .favoriteColor ("grün"). Build (); 

Der einzige Unterschied ist "UserConfig_Cfg" Anstatt von "UserConfig.Cfg "

Anmerkungen

Nachteile:

  • Blinde Erbauer können nicht auf private Mitglieder ihrer Klasse ToBeBuilt zugreifen.
  • Sie sind ausführlicher, da Getter jetzt sowohl im Builder als auch in der Benutzeroberfläche erforderlich sind.
  • Alles für eine einzelne Klasse ist nicht mehr in nur ein Ort.

Das Kompilieren eines Blind Builder ist unkompliziert:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

Die Fieldable -Schnittstelle ist völlig optional

Für eine ToBeBuilt Klasse mit wenigen erforderlichen Feldern - wie diese UserConfig Beispielklasse - könnte der Konstruktor einfach sein

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

Und rief den Baumeister mit

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

Oder sogar durch Eliminieren der Getter (im Builder) insgesamt:

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

Durch die direkte Übergabe von Feldern ist die Klasse ToBeBuilt genauso "blind" (ohne ihren Builder zu kennen) wie die Schnittstelle Fieldable. Für ToBeBuilt Klassen, die "um ein Vielfaches erweitert und erweitert" werden sollen (siehe Titel dieses Beitrags), werden jedoch Änderungen an vorgenommen irgendein Feld erfordert Änderungen in jeder Unterklasse, in jeder Builder und ToBeBuilt Konstruktor. Wenn die Anzahl der Felder und Unterklassen zunimmt, wird es unpraktisch, dies beizubehalten.

(In der Tat kann die Verwendung eines Builders bei wenigen erforderlichen Feldern zu viel des Guten sein. Für Interessenten hier eine Stichprobe von einigen der größeren Fieldable-Schnittstellen in meiner persönlichen Bibliothek.)

Sekundärklassen im Unterpaket

Ich wähle, alle Builder- und die Fieldable -Klassen für alle Blind Builder in einem Unterpaket ihrer ToBeBuilt -Klasse zu haben. Das Unterpaket heißt immer "z". Dies verhindert, dass diese sekundären Klassen die JavaDoc-Paketliste überladen. Zum Beispiel

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

Validierungsbeispiel

Wie oben erwähnt, erfolgt die gesamte Validierung im Konstruktor von ToBeBuilt. Hier ist wieder der Konstruktor mit Beispiel-Validierungscode:

public UserConfig (UserConfig_Fieldable uc_f) {
 //transfer
 try {
 sName = uc_f.getName (); 
} catch (NullPointerException rx) {
 neue NullPointerException ("uc_f") auslösen; 
} 
 iAge = uc_f.getAge (); 
 sFavColor = uc_f.getFavoriteColor (); 
 // validieren (sollte die Muster wirklich vorkompilieren ...) 
 versuchen Sie {
 if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
 Neue IllegalArgumentException auslösen ("uc_f.getName () (" + sName + "") darf nicht leer sein und darf nur Buchstaben, Ziffern und Unterstriche enthalten. "); 
} 
} catch (NullPointerException rx) {
 wirft eine neue NullPointerException ("uc_f.getName ()"); 
} 
 if (iAge <0) {
 Neue IllegalArgumentException auslösen ("uc_f.getAge () (" + iAge + ") ist kleiner als Null."); 
} 
 try {
 if (! Pattern.compile ("(?: rot | blau | grün | pink)"). matcher (sFavColor) .matches ()) {
 wirft eine neue IllegalArgumentException ("uc_f.getFavoriteColor () (\ "" + uc_f.getFavoriteColor () + "\") ist nicht rot, blau, grün oder pink. "); 
} 
} catch (NullPointerException rx) {
 neue NullPointerException auslösen ("uc_f.getFavoriteColor ()"); 
} 
}

Builder dokumentieren

Dieser Abschnitt gilt sowohl für Bloch Builder als auch für Blind Builder. Es zeigt, wie ich die Klassen in diesem Entwurf dokumentiere, indem Setter (im Builder) und ihre Getter (in der Klasse ToBeBuilt) direkt miteinander verknüpft werden - mit einem einzigen Mausklick und ohne Der Benutzer muss wissen, wo sich diese Funktionen tatsächlich befinden - und ohne dass der Entwickler irgendetwas redundant dokumentieren muss.

Getters: Nur in den Klassen ToBeBuilt

Getter werden nur in der Klasse ToBeBuilt dokumentiert. Die äquivalenten Getter beide in der _Fieldable und _Cfg Klassen werden ignoriert. Ich dokumentiere sie überhaupt nicht.

/**
 <P> Das Alter des Benutzers. </ P> 
 @Return Ein int, das das Alter des Benutzers darstellt. 
 @See UserConfig_Cfg # age (int) 
 @see getName () 
 **/
 public int getAge () {
 return iAge; 
}

Der Erste @see ist ein Link zu seinem Setter, der sich in der Builder-Klasse befindet.

Setter: In der Builder-Klasse

Der Setter ist dokumentiert als ob es in der Klasse ToBeBuilt istund auch als ob es führt die Validierung durch (die wirklich vom Konstruktor des ToBeBuilt durchgeführt wird). Das Sternchen ("* ") ist ein visueller Hinweis darauf, dass sich das Ziel des Links in einer anderen Klasse befindet.

/**
 <P> Legen Sie das Alter des Benutzers fest. </ P> 
 @Param i_age Darf nicht kleiner als Null sein. Get with {@code UserConfig # getName () getName ()} *. 
 @See #favoriteColor (String) 
 **/
 Public UserConfig_Cfg age (int i_age) {
 iAge = i_age; 
 gib dies zurück; 
}

Weitere Informationen

Alles zusammen: Die vollständige Quelle des Blind Builder-Beispiels mit vollständiger Dokumentation

UserConfig.Java

java.util.regex.Pattern importieren; 
/** 
 <P> Informationen zu einem Benutzer - <I> [Builder: UserConfig_Cfg] </ I> </ P> 
 <P> Die Validierung aller Felder erfolgt in diesem Klassenkonstruktor. Jede Validierungsanforderung ist jedoch nur in den Setterfunktionen des Builders dokumentiert. </ P> 
 <P> {@code Java xbn.z.xmpl.lang.builder.finalv .UserConfig} </ P> 
 **/
 Öffentliche Klasse UserConfig {
 Public static final void main (String [] igno_red) {
 UserConfig uc = new UserConfig_Cfg ("Kermit"). Alter (50) .favoriteColor ("grün"). Build (); 
 System.out.println (uc); 
} 
 Privates Finale String sName; 
 Private final int iAge; 
 Private final String sFavColor; 
 /**
 <P> Neue Instanz erstellen. Dadurch werden alle Felder festgelegt und validiert . </ P> 
 @Param uc_f Darf nicht {@code null} sein. 
 **/
 Public UserConfig (UserConfig_Fieldable uc_f) {
 // Übertragung 
 try {
 sName = uc_f.getName (); 
} catch (NullPointerException rx) {
 wirf eine neue NullPointerException ("uc_f"); 
} 
 iAge = uc_f.getAge (); 
 sFavColor = uc_f.getFavoriteColor (); 
 //validate
 try {
 if (! Pattern.compile ( "\\ w +"). matcher (sName) .matches ()) {
 neue IllegalArgumentException auslösen ("uc_f.getName () (\" "+ sName +"\") darf nicht leer sein und muss sein Enthält nur Buchstaben, Ziffern und Unterstriche. "); 
} 
} catch (NullPointerException rx) {
 Neue NullPointerException auslösen (" uc_f.getName () "); 
} 
 if (iAge <0) {
 Neue IllegalArgumentException auslösen ("uc_f.getAge () (" + iAge + ") ist kleiner als Null."); 
} 
 try {
 if (! Pattern.compile ("(?: rot | blau | grün | pink)"). matcher (sFavColor) .matches ()) {
 Neue IllegalArgumentException auslösen ("uc_f.getFavoriteColor () (" + uc_f.getFavoriteColor () + "\") ist nicht rot, blau, grün oder pink. "); 
 } 
} catch (NullPointerException rx) {
 Neue NullPointerException auslösen ("uc_f.getFavoriteColor ()"); 
} 
} 
 // getters ... START 
 /**
 <P> Der Name des Benutzers. </ P> 
 @return Eine nicht - {@ code null}, nicht leere Zeichenfolge. 
 @see UserConfig_Cfg # UserConfig_Cfg (String) 
 @see #getAge () 
 @see #getFavoriteColor () 
 **/
 public String getName () {
 return sName; 
} 
 /**
 <P> Das Alter des Benutzers. </ P> 
 @return A. Zahl größer als oder gleich Null. 
 @see UserConfig_Cfg # age (int) 
 @see #getName () 
 **/
 public int getAge () {
 return iAge; 
} 
 /**
 <P> Die Lieblingsfarbe des Benutzers. </ P> 
 @ return Eine nicht - {@ code null}, nicht leere Zeichenfolge. 
 @see UserConfig_Cfg # age (int) 
 @see #getName () 
 **/
 public String getFavoriteColor () {
 return sFavColor; 
} 
 //getters...END
 public String toString () {
 return "getName () =" + getName () + ", getAge () =" + getAge () + ", getFavoriteColor () =" + getFavoriteColor (); 
} 
}

UserConfig_Fieldable.Java

/**
 <P> Erforderlich für den Konstruktor {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable)}. </ P> 
 **/
 Öffentliche Schnittstelle UserConfig_Fieldable {
 String getName (); 
 Int getAge (); 
 String getFavoriteColor (); 
}

UserConfig_Cfg.Java

import Java.util.regex.Pattern; 
/** 
 <P> Builder für {@link UserConfig}. </ P> 
 <P> Die Überprüfung aller Felder erfolgt im Konstruktor <CODE> UserConfig </ CODE>. Jede Validierungsanforderung ist jedoch nur in den Setterfunktionen dieser Klassen dokumentiert. </ P> 
 **/
 Öffentliche Klasse UserConfig_Cfg implementiert UserConfig_Fieldable {
 Public String sName; 
 public int iAge; 
 public String sFavColor; 
 /**
 <P> Erstellen Sie eine neue Instanz mit dem Namen des Benutzers. </ P> 
 @param s_name darf nicht {@code null} oder leer sein und darf nur Buchstaben, Ziffern und Unterstriche enthalten. Get with {@code UserConfig # getName () getName ()} {@ code ()} . 
 **/
 Public UserConfig_Cfg (String s_name) { 
 SName = s_name; 
} 
 // selbstkehrende Setter ... START 
 /**
 <P> Legen Sie das Alter des Benutzers fest . </ P> 
 @Param i_age Darf nicht kleiner als Null sein. Holen Sie sich mit {@code UserConfig # getName () getName ()} {@ code ()} . 
 @Siehe #favoriteColor (String) 
 **/
 public UserConfig_Cfg age (int i_age) {
 iAge = i_age; 
 return this; 
} 
 /**
 < P> Stellen Sie die Lieblingsfarbe des Benutzers ein. </ P> 
 @Param s_color Muss {@code "rot"}, {@code "blau"}, {@code grün} oder {@code "heiß sein Rosa"}. Get with {@code UserConfig # getName () getName ()} {@ code ()} *. 
 @See #age (int) 
 **/
 Public UserConfig_Cfg favorColor (String s_color) {
 SFavColor = s_color; 
 Gibt dies zurück; 
} 
 // Selbstrückgebende Setter ... END 
 // getters ... START 
 public String getName () {
 return sName; 
} 
 public int getAge () {
 return iAge; 
} 
 public String getFavoriteColor () {
 return sFavColor; 
} 
 //getters...END
/** 
 <P> Erstellen Sie die UserConfig wie konfiguriert. </ P> 
 @Return <CODE> (neue {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (this)) </ CODE> 
 **/
 Public UserConfig build () {
 Return (neue UserConfig (this)); 
} 
}
21
aliteralmind

Ich denke, die Frage hier setzt von Anfang an etwas voraus, ohne zu beweisen, dass das Builder-Muster von Natur aus gut ist.

tl; dr Ich denke, das Builder-Muster ist selten wenn überhaupt eine gute Idee.


Builder-Musterzweck

Der Zweck des Builder-Musters besteht darin, zwei Regeln beizubehalten, die das Konsumieren Ihrer Klasse erleichtern:

  1. Objekte sollten nicht in inkonsistenten/unbrauchbaren/ungültigen Zuständen erstellt werden können.

    • Dies bezieht sich auf Szenarien, in denen beispielsweise ein Person -Objekt erstellt werden kann, ohne dass Id ausgefüllt ist, während alle Codeteile, die dieses Objekt verwenden, erfordern können das Id nur um richtig mit dem Person zu arbeiten.
  2. Objektkonstruktoren sollten nicht zu viele Parameter benötigen.

Der Zweck des Builder-Musters ist also unumstritten gut. Ich denke, ein Großteil des Wunsches und der Verwendung basiert auf einer Analyse, die im Grunde genommen so weit gegangen ist: Wir wollen diese beiden Regeln, dies gibt diese beiden Regeln - obwohl ich denke, dass es sich lohnt, andere Wege zu untersuchen, um diese beiden Regeln zu erreichen.


Warum sich andere Ansätze ansehen?

Ich denke, der Grund wird durch die Tatsache dieser Frage selbst gut gezeigt; Es gibt Komplexität und eine Menge Zeremonie, die den Strukturen hinzugefügt wird, wenn das Builder-Muster auf sie angewendet wird. In dieser Frage wird gefragt, wie ein Teil dieser Komplexität gelöst werden kann, da sie wie die Komplexität häufig zu einem Szenario führt, das sich seltsam verhält (erbt). Diese Komplexität erhöht auch den Wartungsaufwand (das Hinzufügen, Ändern oder Entfernen von Eigenschaften ist weitaus komplexer als sonst).


Andere Ansätze

Welche Ansätze gibt es für die obige Regel Nummer eins? Der Schlüssel, auf den sich diese Regel bezieht, ist, dass ein Objekt bei der Erstellung alle Informationen hat, die es benötigt, um ordnungsgemäß zu funktionieren - und nach der Erstellung können diese Informationen nicht extern geändert werden (es handelt sich also um unveränderliche Informationen).

Eine Möglichkeit, einem Objekt bei der Erstellung alle erforderlichen Informationen zu geben, besteht darin, dem Konstruktor einfach Parameter hinzuzufügen. Wenn diese Informationen vom Konstruktor angefordert werden, können Sie dieses Objekt ohne all diese Informationen nicht erstellen. Daher wird es in einen gültigen Zustand versetzt. Was aber, wenn das Objekt viele Informationen benötigt, um gültig zu sein? Oh verdammt, wenn das der Fall ist , würde dieser Ansatz gegen Regel 2 oben verstoßen .

Ok was gibt es sonst noch? Nun, Sie könnten einfach alle Informationen, die erforderlich sind, damit sich Ihr Objekt in einem konsistenten Zustand befindet, in ein anderes Objekt bündeln, das zur Konstruktionszeit aufgenommen wird. Ihr obiger Code anstelle eines Builder-Musters lautet dann:

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

Dies unterscheidet sich nicht wesentlich vom Builder-Muster, obwohl es etwas einfacher ist, und vor allem erfüllen wir jetzt Regel 1 und Regel 2 .

Warum also nicht ein bisschen mehr machen und es zu einem vollwertigen Builder machen? Es ist einfach unnötig . Ich habe beide Zwecke des Builder-Musters in diesem Ansatz mit etwas etwas Einfacherem, leichter zu wartendem, und wiederverwendbar erfüllt. Das letzte Bit ist der Schlüssel. Dieses verwendete Beispiel ist imaginär und eignet sich nicht für semantische Zwecke in der realen Welt. Zeigen wir also, wie dieser Ansatz führt zu einem wiederverwendbaren DTO und nicht zu einer einzelnen Zweckklasse =.

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

Wenn Sie also zusammenhängende DTOs wie diese erstellen, können beide den Zweck des Builder-Musters einfacher und mit einem breiteren Wert/Nutzen erfüllen. Darüber hinaus löst dieser Ansatz die Vererbungskomplexität, zu der das Builder-Muster führt:

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

Möglicherweise ist das DTO nicht immer zusammenhängend, oder um die Gruppierungen von Eigenschaften zusammenhängend zu machen, müssen sie auf mehrere DTOs aufgeteilt werden - dies ist kein wirkliches Problem. Wenn für Ihr Objekt 18 Eigenschaften erforderlich sind und Sie mit diesen Eigenschaften 3 zusammenhängende DTOs erstellen können, haben Sie eine einfache Konstruktion, die den Builder-Zwecken entspricht, und noch einige mehr. Wenn Sie keine zusammenhängenden Gruppierungen erstellen können, ist dies möglicherweise ein Zeichen dafür, dass Ihre Objekte nicht zusammenhängend sind, wenn sie Eigenschaften haben, die so völlig unabhängig sind. Selbst dann ist es aufgrund des einfacheren Implementierungs-Plus immer noch vorzuziehen, ein einzelnes nicht zusammenhängendes DTO zu erstellen Beheben Ihres Vererbungsproblems.


So verbessern Sie das Builder-Muster

Ok, abgesehen von all dem Rimble-Streifzug haben Sie ein Problem und suchen nach einem Design-Ansatz, um es zu lösen. Mein Vorschlag: Das Erben von Klassen kann einfach eine verschachtelte Klasse haben, die von der Builder-Klasse der Superklasse erbt. Die erbende Klasse hat also im Grunde die gleiche Struktur wie die Super-Klasse und ein Builder-Muster, das mit den zusätzlichen Funktionen genau gleich funktionieren sollte für die zusätzlichen Eigenschaften der Unterklasse ..


Wenn es eine gute Idee ist

Abgesehen davon, das Builder-Muster hat eine Nische. Wir alle wissen es, weil wir alle diesen bestimmten Builder an der einen oder anderen Stelle gelernt haben: StringBuilder - hier ist der Zweck keine einfache Konstruktion, da Strings nicht einfacher zu konstruieren und zu verketten sind usw. Dies ist ein großartiger Builder weil es einen Leistungsvorteil hat.

Der Leistungsvorteil ist somit: Sie haben eine Reihe von Objekten, sie sind unveränderlich, Sie müssen sie auf ein Objekt eines unveränderlichen Typs reduzieren. Wenn Sie es schrittweise tun, werden hier viele Zwischenobjekte erstellt, sodass alles auf einmal weitaus leistungsfähiger und idealer ist.

Ich denke also, der Schlüssel von , wenn es eine gute Idee ist , liegt in der Problemdomäne des StringBuilder: Mehrere Instanzen unveränderlicher Typen müssen in eine einzelne Instanz eines unveränderlichen Typs umgewandelt werden .

13
Jimmy Hoffa