Ich habe das geschaffen, was für mich eine große Verbesserung gegenüber Josh Blochs Builder Pattern darstellt. Ganz zu schweigen davon, dass es "besser" ist, nur dass es in einer ganz bestimmten Situation einige Vorteile bietet - das größte ist, dass es den Bauherrn von seiner zu bauenden Klasse entkoppelt.
Ich habe diese Alternative, die ich als Blind Builder Pattern bezeichne, im Folgenden ausführlich dokumentiert.
Entwurfsmuster: Blind Builder
Als Alternative zu Joshua Blochs Builder-Muster (Punkt 2 in Effective Java, 2. Ausgabe) habe ich das sogenannte "Blind Builder-Muster" erstellt, das viele der Vorteile des Bloch Builder teilt und neben einem einzelnen Charakter wird genauso verwendet. Blinde Bauherren haben den Vorteil von
- den Builder von seiner einschließenden Klasse zu entkoppeln, wodurch eine zirkuläre Abhängigkeit beseitigt wird,
- stark reduziert die Größe des Quellcodes (was nicht mehr ist ) der einschließenden Klasse, und
- Ermöglicht das
ToBeBuilt
Erweitern der Klasse, ohne dass der Builder erweitert werden muss .
In dieser Dokumentation werde ich die Klasse, die erstellt wird, als " ToBeBuilt
" Klasse bezeichnen.
Eine Klasse, die mit einem Bloch Builder implementiert wurde
Ein Bloch Builder ist ein public static class
Bestandteil der Klasse, die er 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
Versuchen {
sName = uc_c.sName;
} catch (NullPointerException rx) {
werfen neue NullPointerException ("uc_c");
}
iAge = uc_c.iAge;
sFavColor = uc_c.sFavColor;
// ALLE FELDER HIER GÜLTIG MACHEN
}
public String toString () {
return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
}
//builder...START
öffentliche statische Klasse Cfg {
private String sName;
private int iAge;
private String sFavColor;
public Cfg (String s_name) {
sName = s_name;
}
// selbst zurückkehrende Setter ... START
öffentliches Cfg-Alter (int i_age) {
iAge = i_age;
gib das zurück;
}
public Cfg favoriteColor (String s_color) {
sFavColor = s_color;
gib das zurück;
}
// selbst zurückkehrende Setter ... END
public UserConfig build () {
return (new UserConfig (this));
}
}
//Builder ... END
}
Instanziieren einer Klasse mit einem Bloch Builder
UserConfig uc = new UserConfig.Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();
Dieselbe Klasse, implementiert als Blind Builder
Ein Blind Builder besteht aus drei Teilen, die sich jeweils in einer separaten Quellcodedatei befinden:
- Die
ToBeBuilt
Klasse (in diesem Beispiel: UserConfig
)
- Seine "
Fieldable
" Schnittstelle
- Der Bauarbeiter
1. Die zu bauende Klasse
Die zu erstellende Klasse akzeptiert ihre Fieldable
Schnittstelle als einzigen Konstruktorparameter. Der Konstruktor setzt alle internen Felder daraus und validiert sie . 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
Versuchen {
sName = uc_f.getName ();
} catch (NullPointerException rx) {
werfen neue NullPointerException ("uc_f");
}
iAge = uc_f.getAge ();
sFavColor = uc_f.getFavoriteColor ();
// ALLE FELDER HIER GÜLTIG MACHEN
}
public String toString () {
return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
}
}
Wie von einem Smart - Kommentator bemerkt (die aus unerklärlichen Gründen ihre Antwort gelöscht), wenn die ToBeBuilt
Klasse implementiert auch seinen Fieldable
, sein ein-und-nur - Konstruktor kann sowohl als seinen primären verwendet werden und Copy - Konstruktor (ein Nachteil ist , dass die Felder sind zwar immer validiert, auch Es ist bekannt, dass die Felder im Original ToBeBuilt
gültig sind.
2. Die Fieldable
Schnittstelle " "
Die feldfähige Schnittstelle ist die "Brücke" zwischen der ToBeBuilt
Klasse und ihrem Builder und definiert alle Felder, die zum Erstellen des Objekts erforderlich sind. Diese Schnittstelle wird vom ToBeBuilt
Klassenkonstruktor benötigt und vom Builder implementiert. Da diese Schnittstelle von anderen Klassen als dem Builder implementiert werden kann, kann jede Klasse die ToBeBuilt
Klasse leicht instanziieren , ohne gezwungen zu sein, ihren Builder zu verwenden. Dies erleichtert auch das Erweitern der ToBeBuilt
Klasse, wenn das Erweitern des Builders nicht erwünscht oder erforderlich ist.
Wie in einem der folgenden Abschnitte beschrieben, dokumentiere ich die Funktionen in dieser Benutzeroberfläche überhaupt nicht.
öffentliche Schnittstelle UserConfig_Fieldable {
String getName ();
int getAge ();
String getFavoriteColor ();
}
3. Der Erbauer
Der Builder implementiert die Fieldable
Klasse. Es findet überhaupt keine Validierung statt, 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 nach dem ToBeBuilt
Aufruf des Konstruktors des Konstruktors erfolgt. Dies ist wichtig, da ein anderer Thread den Builder möglicherweise weiter manipulieren kann, bevor er an den ToBeBuilt
Konstruktor des Threads ü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 ToBeBuilt
Klasse die endgültige Prüfung durchführt.
Schließlich wird , wie mit der Fieldable
Schnittstelle, dokumentiere ich nichts von seinen 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;
}
// selbst zurückkehrende Setter ... START
public UserConfig_Cfg age (int i_age) {
iAge = i_age;
gib das zurück;
}
public UserConfig_Cfg favoriteColor (String s_color) {
sFavColor = s_color;
gib das zurück;
}
// selbst zurückkehrende 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 (new UserConfig (this));
}
}
Instanziieren einer Klasse mit einem Blind Builder
UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();
Der einzige Unterschied ist " UserConfig_Cfg
" statt " UserConfig.Cfg
"
Anmerkungen
Nachteile:
- Blinde Bauherren können nicht auf private Mitglieder ihrer
ToBeBuilt
Klasse zugreifen.
- Sie sind ausführlicher, da jetzt sowohl im Builder als auch in der Schnittstelle Getter erforderlich sind.
- Alles für eine Klasse ist nicht mehr nur an einem Ort .
Das Kompilieren eines Blind Builder ist einfach:
ToBeBuilt_Fieldable
ToBeBuilt
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 beim Baumeister mit angerufen
public UserConfig build () {
return (new 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 ToBeBuilt
Klasse genauso "blind" (ohne Kenntnis ihres Builders) wie bei der Fieldable
Schnittstelle. Für ToBeBuilt
Klassen, die "um ein Vielfaches erweitert und sub-erweitert" werden sollen (wie im Titel dieses Beitrags angegeben), erfordert jede Änderung in einem beliebigen Feld Änderungen in jeder Unterklasse, in jedem Builder und ToBeBuilt
Konstruktor. Wenn die Anzahl der Felder und Unterklassen zunimmt, ist dies unpraktisch zu pflegen.
(In der Tat ist die Verwendung eines Builders mit wenigen erforderlichen Feldern möglicherweise zu viel des Guten . Für Interessenten finden Sie hier eine Auswahl der größeren Fieldable-Schnittstellen in meiner persönlichen Bibliothek.)
Sekundärklassen im Unterpaket
Ich entscheide mich dafür, 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. Beispielsweise
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 ToBeBuilt
Konstruktor des Benutzers. Hier ist noch einmal der Konstruktor mit Beispielvalidierungscode:
public UserConfig (UserConfig_Fieldable uc_f) {
//Transfer
Versuchen {
sName = uc_f.getName ();
} catch (NullPointerException rx) {
werfen neue NullPointerException ("uc_f");
}
iAge = uc_f.getAge ();
sFavColor = uc_f.getFavoriteColor ();
// validieren (sollte die Muster wirklich vorkompilieren ...)
Versuchen {
if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
throw new IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") darf nicht leer sein und darf nur Ziffern und Unterstriche enthalten.");
}
} catch (NullPointerException rx) {
neue NullPointerException ("uc_f.getName ()") auslösen;
}
if (iAge <0) {
throw new IllegalArgumentException ("uc_f.getAge () (" + iAge + ") ist kleiner als Null.");
}
Versuchen {
if (! Pattern.compile ("(?: rot | blau | grün | pink)"). matcher (sFavColor) .matches ()) {
throw new IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") ist nicht rot, blau, grün oder pink.");
}
} catch (NullPointerException rx) {
werfen neue NullPointerException ("uc_f.getFavoriteColor ()");
}
}
Builder dokumentieren
Dieser Abschnitt gilt sowohl für Bloch Builder als auch für Blind Builder. Es wird gezeigt, wie ich die Klassen in diesem Entwurf dokumentiere und Setter (im Builder) und ihre Getter (in der ToBeBuilt
Klasse) direkt miteinander in Querverweis setzten - mit einem einzigen Mausklick und ohne dass der Benutzer wissen muss, wo diese Funktionen sind tatsächlich vorhanden - und ohne dass der Entwickler irgendetwas redundant dokumentieren muss.
Getter: ToBeBuilt
Nur in den Klassen
Getter werden nur in der ToBeBuilt
Klasse dokumentiert . Die entsprechenden Getter in den Klassen _Fieldable
und
_Cfg
werden ignoriert. Ich dokumentiere sie überhaupt nicht.
/ **
<P> Das Alter des Benutzers. </ P>
@return Ein Int, der das Alter des Benutzers angibt.
@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 wird so dokumentiert, als ob er sich in der ToBeBuilt
Klasse befindet , und auch als ob er die Validierung durchführt (was wirklich vom ToBeBuilt
Konstruktor des Setters durchgeführt wird ). Das Sternchen (" *
") ist ein visueller Hinweis darauf, dass sich das Ziel des Links in einer anderen Klasse befindet.
/ **
<P> Stellen Sie das Alter des Benutzers ein. </ P>
@param i_age Darf nicht kleiner als Null sein. Holen Sie sich mit {@code UserConfig # getName () getName ()} *.
@see #favoriteColor (String)
** /
public UserConfig_Cfg age (int i_age) {
iAge = i_age;
gib das zurück;
}
Weitere Informationen
Alles zusammenfassen: Die vollständige Quelle des Blind Builder-Beispiels mit vollständiger Dokumentation
UserConfig.java
import java.util.regex.Pattern;
/ **
<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"). Age (50) .favoriteColor ("green"). Build ();
System.out.println (uc);
}
private final String sName;
private final int iAge;
private final String sFavColor;
/ **
<P> Erstellen Sie eine neue Instanz. Hiermit werden alle Felder festgelegt und überprüft. </ P>
@param uc_f Darf nicht {@code null} sein.
** /
public UserConfig (UserConfig_Fieldable uc_f) {
//Transfer
Versuchen {
sName = uc_f.getName ();
} catch (NullPointerException rx) {
werfen neue NullPointerException ("uc_f");
}
iAge = uc_f.getAge ();
sFavColor = uc_f.getFavoriteColor ();
//bestätigen
Versuchen {
if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
throw new IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") darf nicht leer sein und darf nur Ziffern und Unterstriche enthalten.");
}
} catch (NullPointerException rx) {
neue NullPointerException ("uc_f.getName ()") auslösen;
}
if (iAge <0) {
throw new IllegalArgumentException ("uc_f.getAge () (" + iAge + ") ist kleiner als Null.");
}
Versuchen {
if (! Pattern.compile ("(?: rot | blau | grün | pink)"). matcher (sFavColor) .matches ()) {
throw new IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") ist nicht rot, blau, grün oder pink.");
}
} catch (NullPointerException rx) {
werfen neue NullPointerException ("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 Eine 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 {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable) -Konstruktor}. </ 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 <CODE> UserConfig </ CODE> -Konstruktor. Jede Validierungsanforderung ist jedoch nur in diesen Klasseneinstellungsfunktionen dokumentiert. </ P>
** /
public class 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. Holen Sie sich mit {@code UserConfig # getName () getName ()} {@code ()} .
** /
public UserConfig_Cfg (String s_name) {
sName = s_name;
}
// selbst zurückkehrende Setter ... START
/ **
<P> Stellen Sie das Alter des Benutzers ein. </ P>
@param i_age Darf nicht kleiner als Null sein. Holen Sie sich mit {@code UserConfig # getName () getName ()} {@code ()} .
@see #favoriteColor (String)
** /
public UserConfig_Cfg age (int i_age) {
iAge = i_age;
gib das zurück;
}
/ **
<P> Stellen Sie die Lieblingsfarbe des Benutzers ein. </ P>
@param s_color Muss {@code "red"}, {@code "blue"}, {@code green} oder {@code "hot pink"} sein. Holen Sie sich mit {@code UserConfig # getName () getName ()} {@code ()} *.
@see #age (int)
** /
public UserConfig_Cfg favoriteColor (String s_color) {
sFavColor = s_color;
gib das zurück;
}
// selbst zurückkehrende 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> (new {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (this)) </ CODE>
** /
public UserConfig build () {
return (new UserConfig (this));
}
}