J'ai créé ce qui, pour moi, représente une grande amélioration par rapport au modèle de constructeur de Josh Bloch. Cela ne veut en aucun cas dire que c’est «meilleur», mais seulement que dans une situation très spécifique , il offre certains avantages, le plus important étant qu’il sépare le constructeur de sa classe en construction.
J'ai documenté en détail cette alternative ci-dessous, que j'appelle le modèle de constructeur aveugle.
Modèle de conception: Builder aveugle
En guise d'alternative au motif Builder de Joshua Bloch (élément 2 dans Effective Java, 2e édition), j'ai créé ce que j'appelle le "motif aveugle Builder", qui partage bon nombre des avantages du constructeur Bloch et, à l'exception d'un seul personnage, est utilisé exactement de la même manière. Les aveugles constructeurs ont l'avantage de
- découpler le constructeur de sa classe englobante, en éliminant une dépendance circulaire,
- réduit considérablement la taille du code source de (ce qui n’est plus ) la classe englobante, et
- permet à la
ToBeBuilt
classe d'être étendue sans avoir à étendre son constructeur .
Dans cette documentation, je parlerai de la classe "en cours de construction ToBeBuilt
".
Une classe implémentée avec un Bloch Builder
Un constructeur de bloch est un public static class
contenu à l'intérieur de la classe qu'il construit. Un exemple:
Classe publique UserConfig {
private final String sName;
finale privée int iAge;
private final String sFavColor;
public UserConfig (UserConfig.Cfg uc_c) {// CONSTRUCTOR
//transfert
essayer {
sName = uc_c.sName;
} catch (NullPointerException rx) {
lancer la nouvelle NullPointerException ("uc_c");
}
iAge = uc_c.iAge;
sFavColor = uc_c.sFavColor;
// VALIDER TOUS LES CHAMPS ICI
}
chaîne publique toString () {
return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
}
//builder...START
classe statique publique Cfg {
private String sName;
int iAge privé;
private String sFavColor;
public Cfg (String s_name) {
sName = s_name;
}
// les setters qui reviennent d'eux-mêmes ...
public Cfg age (int i_age) {
iAge = i_age;
retournez ceci;
}
public Cfg favoriteColor (String s_color) {
sFavColor = s_color;
retournez ceci;
}
// setters auto-retournés ... FIN
public UserConfig build () {
return (new UserConfig (this));
}
}
//builder...END
}
Instanciation d'une classe avec un constructeur de bloch
UserConfig uc = new UserConfig.Cfg ("Kermit"). Age (50) .favoriteColor ("vert"). Build ();
La même classe, implémentée en tant que constructeur aveugle
Blind Builder est composé de trois parties, chacune d’elles se trouvant dans un fichier de code source distinct:
- La
ToBeBuilt
classe (dans cet exemple: UserConfig
)
- Son
Fieldable
interface " "
- Le constructeur
1. La classe à construire
La classe à construire accepte son Fieldable
interface en tant que paramètre constructeur unique. Le constructeur définit tous les champs internes à partir de celui-ci et les valide . Plus important encore, cette ToBeBuilt
classe n'a aucune connaissance de son constructeur.
Classe publique UserConfig {
private final String sName;
finale privée int iAge;
private final String sFavColor;
public UserConfig (UserConfig_Fieldable uc_f) {// CONSTRUCTOR
//transfert
essayer {
sName = uc_f.getName ();
} catch (NullPointerException rx) {
lancer la nouvelle NullPointerException ("uc_f");
}
iAge = uc_f.getAge ();
sFavColor = uc_f.getFavoriteColor ();
// VALIDER TOUS LES CHAMPS ICI
}
chaîne publique toString () {
return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
}
}
Comme l'a noté un commentateur intelligent (qui a inexplicablement supprimé sa réponse), si la ToBeBuilt
classe l'implémentait également Fieldable
, son constructeur one-and-only peut être utilisé à la fois comme constructeur primaire et constructeur (un inconvénient est que les champs sont toujours validés, même si on sait que les champs de l’original ToBeBuilt
sont valides).
2. L' Fieldable
interface " "
L'interface pouvant être remplie est le "pont" entre la ToBeBuilt
classe et son constructeur, définissant tous les champs nécessaires à la construction de l'objet. Cette interface est requise par le ToBeBuilt
constructeur de classes et est implémentée par le générateur. Etant donné que cette interface peut être implémentée par des classes autres que le constructeur, toute classe peut facilement instancier la ToBeBuilt
classe sans être obligée d'utiliser son générateur. Cela facilite également l’extension de la ToBeBuilt
classe, lorsque l’extension de son générateur n’est ni souhaitable ni nécessaire.
Comme décrit dans la section ci-dessous, je ne documente pas du tout les fonctions de cette interface.
interface publique UserConfig_Fieldable {
String getName ();
int getAge ();
String getFavoriteColor ();
}
3. Le constructeur
Le constructeur implémente la Fieldable
classe. Il ne fait aucune validation et, pour souligner ce fait, tous ses domaines sont publics et modifiables. Bien que cette accessibilité publique ne soit pas une exigence, je la préfère et la recommande, car elle renforce le fait que la validation ne se produit pas avant que le ToBeBuilt
constructeur de s soit appelé. Ceci est important, car il est possible qu'un autre thread manipule davantage le générateur avant qu'il ne soit passé au ToBeBuilt
constructeur de. La seule façon de garantir la validité des champs (en supposant que le constructeur ne puisse pas en quelque sorte "verrouiller" son état) est que la ToBeBuilt
classe effectue le contrôle final.
Enfin, comme pour l' Fieldable
interface, je ne documente aucun de ses accesseurs.
La classe publique UserConfig_Cfg implémente UserConfig_Fieldable {
public String sName;
public int iAge;
public String sFavColor;
public UserConfig_Cfg (String s_name) {
sName = s_name;
}
// les setters qui reviennent d'eux-mêmes ...
public UserConfig_Cfg age (int i_age) {
iAge = i_age;
retournez ceci;
}
public UserConfig_Cfg favoriteColor (String s_color) {
sFavColor = s_color;
retournez ceci;
}
// setters auto-retournés ... FIN
//getters...START
public String getName () {
renvoyer sName;
}
public int getAge () {
retourner iAge;
}
public String getFavoriteColor () {
retourne sFavColor;
}
//getters...END
public UserConfig build () {
return (new UserConfig (this));
}
}
Instanciation d'une classe avec un constructeur aveugle
UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("vert"). Build ();
La seule différence est " UserConfig_Cfg
" au lieu de " UserConfig.Cfg
"
Remarques
Désavantages:
- Blind Builders ne peut pas accéder aux membres privés de sa
ToBeBuilt
classe,
- Ils sont plus explicites, car les getters sont maintenant nécessaires dans le constructeur et dans l'interface.
- Tout pour une seule classe ne se trouve plus dans un seul endroit .
Compiler un constructeur aveugle est simple:
ToBeBuilt_Fieldable
ToBeBuilt
ToBeBuilt_Cfg
L' Fieldable
interface est entièrement optionnelle
Pour une ToBeBuilt
classe avec peu de champs obligatoires - comme cet UserConfig
exemple de classe, le constructeur peut simplement être
public UserConfig (String s_name, int i_age, String s_favColor) {
Et appelé dans le constructeur avec
public UserConfig build () {
return (new UserConfig (getName (), getAge (), getFavoriteColor ()));
}
Ou même en éliminant les getters (dans le constructeur):
return (new UserConfig (sName, iAge, sFavoriteColor));
En passant directement des champs, la ToBeBuilt
classe est aussi "aveugle" (ignorant son constructeur) qu'elle l'est avec l' Fieldable
interface. Cependant, pour les ToBeBuilt
classes et sont destinées à être « étendu et plusieurs fois sous-étendu » (qui est dans le titre de ce post), toute modification apportée à tout terrain nécessite des changements dans tous les sous-classe, dans chaque constructeur et ToBeBuilt
constructeur. Au fur et à mesure que le nombre de champs et de sous-classes augmente, il devient impossible de le gérer.
(En effet, avec peu de champs nécessaires, utiliser un constructeur peut être excessif. Pour les personnes intéressées, voici un échantillon de certaines des interfaces Fieldable les plus grandes de ma bibliothèque personnelle.)
Classes secondaires en sous-package
Je choisis d'avoir tous les constructeurs et les Fieldable
classes, pour tous les constructeurs aveugles, dans un sous-package de leur ToBeBuilt
classe. Le sous-package est toujours nommé " z
". Cela empêche ces classes secondaires d’encombrer la liste de packages JavaDoc. Par exemple
library.class.my.UserConfig
library.class.my.z.UserConfig_Fieldable
library.class.my.z.UserConfig_Cfg
Exemple de validation
Comme mentionné ci-dessus, toute la validation a lieu dans le ToBeBuilt
constructeur de '. Voici à nouveau le constructeur avec un exemple de code de validation:
public UserConfig (UserConfig_Fieldable uc_f) {
//transfert
essayer {
sName = uc_f.getName ();
} catch (NullPointerException rx) {
lancer la nouvelle NullPointerException ("uc_f");
}
iAge = uc_f.getAge ();
sFavColor = uc_f.getFavoriteColor ();
// valider (devrait vraiment pré-compiler les patterns ...)
essayer {
if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
lancer une nouvelle exception IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") peut ne pas être vide et ne doit contenir que des lettres, des chiffres et des caractères de soulignement.");
}
} catch (NullPointerException rx) {
jette new NullPointerException ("uc_f.getName ()");
}
si (iAge <0) {
jette new IllegalArgumentException ("uc_f.getAge () (" (+ iAge + ") est inférieur à zéro.");
}
essayer {
if (! Pattern.compile ("(?: rouge | bleu | vert | rose vif)"). matcher (sFavColor) .matches ()) {
jette la nouvelle IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") n'est pas rouge, bleu, vert ou rose vif.");
}
} catch (NullPointerException rx) {
jette new NullPointerException ("uc_f.getFavoriteColor ()");
}
}
Documenter les constructeurs
Cette section s’applique aux constructeurs Bloch et aux aveugles. Cela montre comment je documente les classes dans cette conception, en faisant en sorte que les setters (dans le constructeur) et leurs getters (dans la ToBeBuilt
classe) se référencent directement, avec un simple clic de souris, sans que l'utilisateur ait besoin de savoir où. ces fonctions résident réellement - et sans que le développeur ait à documenter quoi que ce soit de manière redondante.
Getters: Dans les ToBeBuilt
classes seulement
Les accesseurs ne sont documentés que dans la ToBeBuilt
classe. Les accesseurs équivalents dans les classes _Fieldable
et
_Cfg
sont ignorés. Je ne les documente pas du tout.
/ **
<P> L'âge de l'utilisateur. </ P>
@return Un int représentant l'âge de l'utilisateur.
@see UserConfig_Cfg # age (int)
@voir getName ()
** /
public int getAge () {
retourner iAge;
}
Le premier @see
est un lien vers son créateur, qui se trouve dans la classe de générateur.
Setters: dans la classe des constructeurs
Le compositeur est documenté comme il est dans la ToBeBuilt
classe , et aussi comme il fait la validation (qui est vraiment fait par le ToBeBuilt
constructeur de). L'astérisque (" *
") est un indice visuel indiquant que la cible du lien est dans une autre classe.
/ **
<P> Définir l'âge de l'utilisateur. </ P>
@param i_age Ne peut être inférieur à zéro. Obtenez avec {@code UserConfig # getName () getName ()} *.
@see #favoriteColor (String)
** /
public UserConfig_Cfg age (int i_age) {
iAge = i_age;
retournez ceci;
}
Plus d'informations
Rassembler tout cela: source complète de l'exemple de Blind Builder, avec documentation complète
UserConfig.java
importer java.util.regex.Pattern;
/ **
<P> Informations sur un utilisateur - <I> [constructeur: UserConfig_Cfg] </ I> </ P>
<P> La validation de tous les champs se produit dans ce constructeur de classes. Cependant, chaque exigence de validation est un document uniquement dans les fonctions de définition du générateur. </ P>
<P> {@ code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </ P>
** /
Classe publique UserConfig {
public statique final void main (String [] igno_red) {
UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("vert"). Build ();
System.out.println (uc);
}
private final String sName;
finale privée int iAge;
private final String sFavColor;
/ **
<P> Créer une nouvelle instance. Ceci définit et valide tous les champs. </ P>
@param uc_f Peut ne pas être {@code null}.
** /
public UserConfig (UserConfig_Fieldable uc_f) {
//transfert
essayer {
sName = uc_f.getName ();
} catch (NullPointerException rx) {
lancer la nouvelle NullPointerException ("uc_f");
}
iAge = uc_f.getAge ();
sFavColor = uc_f.getFavoriteColor ();
//valider
essayer {
if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
lancer une nouvelle exception IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") peut ne pas être vide et ne doit contenir que des lettres, des chiffres et des caractères de soulignement.");
}
} catch (NullPointerException rx) {
jette new NullPointerException ("uc_f.getName ()");
}
si (iAge <0) {
jette new IllegalArgumentException ("uc_f.getAge () (" (+ iAge + ") est inférieur à zéro.");
}
essayer {
if (! Pattern.compile ("(?: rouge | bleu | vert | rose vif)"). matcher (sFavColor) .matches ()) {
jette la nouvelle IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") n'est pas rouge, bleu, vert ou rose vif.");
}
} catch (NullPointerException rx) {
jette new NullPointerException ("uc_f.getFavoriteColor ()");
}
}
//getters...START
/ **
<P> Nom de l'utilisateur. </ P>
@return Chaîne non - {@ code null}, non vide.
@see UserConfig_Cfg # UserConfig_Cfg (Chaîne)
@see #getAge ()
@see #getFavoriteColor ()
** /
public String getName () {
renvoyer sName;
}
/ **
<P> L'âge de l'utilisateur. </ P>
@retour Un nombre supérieur ou égal à zéro.
@see UserConfig_Cfg # age (int)
@voir #getName ()
** /
public int getAge () {
retourner iAge;
}
/ **
<P> La couleur préférée de l'utilisateur. </ P>
@return Chaîne non - {@ code null}, non vide.
@see UserConfig_Cfg # age (int)
@voir #getName ()
** /
public String getFavoriteColor () {
retourne sFavColor;
}
//getters...END
chaîne publique toString () {
retourne "getName () =" + getName () + ", getAge () =" + getAge () + ", getFavoriteColor () =" + getFavoriteColor ();
}
}
UserConfig_Fieldable.java
/ **
<P> Requis par le constructeur {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable)}. </ P>
** /
interface publique UserConfig_Fieldable {
String getName ();
int getAge ();
String getFavoriteColor ();
}
UserConfig_Cfg.java
importer java.util.regex.Pattern;
/ **
<P> Constructeur pour {@link UserConfig}. </ P>
<P> La validation de tous les champs a lieu dans le constructeur <CODE> UserConfig </ CODE>. Toutefois, chaque exigence de validation n’est documentée que dans les fonctions de définition de cette classe. </ P>
** /
La classe publique UserConfig_Cfg implémente UserConfig_Fieldable {
public String sName;
public int iAge;
public String sFavColor;
/ **
<P> Créer une nouvelle instance avec le nom de l'utilisateur. </ P>
@param s_name Peut ne pas être {@code null} ou vide, il doit contenir uniquement des lettres, des chiffres et des traits de soulignement. Obtenez avec {@code UserConfig # getName () getName ()} {@ code ()} .
** /
public UserConfig_Cfg (String s_name) {
sName = s_name;
}
// les setters qui reviennent d'eux-mêmes ...
/ **
<P> Définir l'âge de l'utilisateur. </ P>
@param i_age Ne peut être inférieur à zéro. Obtenez avec {@code UserConfig # getName () getName ()} {@ code ()} .
@see #favoriteColor (String)
** /
public UserConfig_Cfg age (int i_age) {
iAge = i_age;
retournez ceci;
}
/ **
<P> Définissez la couleur préférée de l'utilisateur. </ P>
@param s_color Doit être {@code "red"}, {@code "blue"}, {@code green} ou {@code "hot pink"}. Obtenez avec {@code UserConfig # getName () getName ()} {@ code ()} *.
@see #age (int)
** /
public UserConfig_Cfg favoriteColor (String s_color) {
sFavColor = s_color;
retournez ceci;
}
// setters auto-retournés ... FIN
//getters...START
public String getName () {
renvoyer sName;
}
public int getAge () {
retourner iAge;
}
public String getFavoriteColor () {
retourne sFavColor;
}
//getters...END
/ **
<P> Construisez le UserConfig, tel que configuré. </ P>
@return <CODE> (new {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (this)) </ CODE>
** /
public UserConfig build () {
return (new UserConfig (this));
}
}