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));
}
}
asImmutable
et incluez-le dans l'ReadableFoo
interface [en utilisant cette philosophie, l'appelbuild
d'un objet immuable renverrait simplement une référence au même objet].*_Fieldable
et ajouter de nouveaux accesseurs, et étendre le*_Cfg
, et ajouter de nouveaux ajusteurs, mais je ne vois pas pourquoi vous auriez besoin de reproduire des accesseurs existants. Ils sont hérités et, à moins qu'ils n'aient besoin de fonctionnalités différentes, il n'est pas nécessaire de les recréer.Je pense que la question ici suppose quelque chose dès le départ sans essayer de le prouver, que le modèle de constructeur est intrinsèquement bon.
Je pense que le modèle de construction est rarement, voire jamais, une bonne idée.
But du motif constructeur
L'objectif du modèle de générateur est de gérer deux règles qui faciliteront l'utilisation de votre classe:
Les objets ne doivent pas pouvoir être construits dans des états inconsistant / inutilisable / invalide.
Person
objet peut être construit sans l' avoir estId
rempli, alors que tous les morceaux de code que l' utilisation de cet objet peut exiger laId
juste bien travailler avec lePerson
.Les constructeurs d'objets ne devraient pas nécessiter trop de paramètres .
Ainsi, l'objectif du modèle de construction n'est pas controversé. Je pense qu'une grande partie de son désir et de son utilisation est basée sur une analyse qui a été poussée jusque-là: nous voulons ces deux règles, cela donne ces deux règles - bien que je pense qu'il vaut la peine de rechercher d'autres moyens de les appliquer.
Pourquoi s'embêter à regarder d'autres approches?
Je pense que la raison est bien démontrée par le fait de cette question elle-même; l'application du modèle de construction est complexe et les cérémonies complexes. Cette question demande comment résoudre une partie de cette complexité car, si elle est complexe, elle crée un scénario qui se comporte étrangement (héritage). Cette complexité augmente également les coûts de maintenance (l'ajout, la modification ou la suppression de propriétés est bien plus complexe qu'autrement).
Autres approches
Donc, pour la règle numéro un ci-dessus, quelles sont les approches? La règle à laquelle cette règle fait référence est que, lors de la construction, un objet dispose de toutes les informations nécessaires pour fonctionner correctement. Après la construction, ces informations ne peuvent plus être modifiées en externe (elles sont donc immuables).
Une façon de donner toutes les informations nécessaires à un objet lors de la construction consiste simplement à ajouter des paramètres au constructeur. Si cette information est demandée par le constructeur, vous ne pourrez pas construire cet objet sans toutes ces informations, il sera donc construit dans un état valide. Mais que se passe-t-il si l'objet nécessite beaucoup d'informations pour être valide? Oh dang, si tel est le cas, cette approche enfreindrait la règle n ° 2 ci-dessus .
Ok qu'est-ce qu'il y a d'autre? Eh bien, vous pouvez simplement prendre toutes les informations nécessaires pour que votre objet soit dans un état cohérent et les regrouper dans un autre objet pris au moment de la construction. Votre code ci-dessus au lieu d'avoir un modèle de générateur serait alors:
Ce n'est pas très différent du motif de construction, bien qu'il soit un peu plus simple et, plus important encore, nous satisfaisons maintenant les règles n ° 1 et n ° 2 .
Alors, pourquoi ne pas aller plus loin et en faire un constructeur complet? C'est simplement inutile . Dans cette approche, j’ai satisfait les deux objectifs du modèle de construction avec quelque chose de légèrement plus simple, plus facile à gérer et réutilisable . Ce dernier bit est la clé, cet exemple utilisé est imaginaire et ne se prête pas à une utilisation sémantique réelle. Nous allons donc montrer comment cette approche produit un DTO réutilisable plutôt qu'une classe à usage unique .
Ainsi, lorsque vous créez des DTO cohésifs comme celui-ci, ils peuvent à la fois remplir l'objectif du modèle de construction, et avoir une valeur / utilité plus large. De plus, cette approche résout la complexité de l'héritage du modèle de construction:
Vous constaterez peut-être que le DTO n'est pas toujours cohérent ou que, pour rendre cohérents les regroupements de propriétés, ils doivent être dissociés entre plusieurs DTO - ce n'est pas vraiment un problème. Si votre objet nécessite 18 propriétés et que vous pouvez créer 3 objets DTO cohésifs avec ces propriétés, vous disposez d'une construction simple qui répond aux besoins des constructeurs, puis à d'autres. Si vous ne parvenez pas à créer des groupements cohérents, cela peut être un signe que vos objets ne sont pas cohérents s'ils ont des propriétés totalement indépendantes les unes des autres - mais même dans ce cas, il est préférable de créer un seul DTO non cohérent en raison de la simplicité de son implémentation, plus résoudre votre problème d'héritage.
Comment améliorer le motif de construction
Ok, donc, tous les problèmes à résoudre, vous avez un problème et vous recherchez une approche de conception pour le résoudre. Ma suggestion: les classes qui héritent peuvent simplement avoir une classe imbriquée qui hérite de la classe du générateur de la super-classe, de sorte que la classe qui hérite a la même structure que la super-classe et possède un modèle de générateur qui doit fonctionner exactement de la même façon avec les fonctions supplémentaires. pour les propriétés supplémentaires de la sous-classe ..
Quand c'est une bonne idée
Ranting côté, le modèle de constructeur a une place . Nous le savons tous parce que nous avons tous appris ce constructeur en particulier à un moment ou à un autre:
StringBuilder
- ici, le but n'est pas une construction simple, car les chaînes ne pourraient pas être plus faciles à construire et à concaténer, etc. C'est un excellent constructeur car il offre un avantage en termes de performances .L'avantage en termes de performances est donc le suivant: vous avez un tas d'objets, ils sont d'un type immuable, vous devez les réduire à un objet d'un type immuable. Si vous le faites progressivement, vous créerez ici plusieurs objets intermédiaires. Il est donc beaucoup plus performant et idéal de tout faire en même temps.
Donc, je pense que la clé de quand c'est une bonne idée est dans le domaine de problèmes de
StringBuilder
: Nécessité de transformer plusieurs instances de types immuables en une seule instance d'un type immuable .la source
fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()
propose une API succincte pour la construction de foos et peut offrir une vérification des erreurs réelles dans le constructeur lui-même. Sans le constructeur, l'objet lui-même doit vérifier ses entrées, ce qui signifie que nous ne sommes pas mieux lotis qu'auparavant.Fieldable
paramètre. Je qualifierais cette fonction de validation duToBeBuilt
constructeur, mais il pourrait être appelé par quoi que ce soit, où que vous soyez. Cela élimine le potentiel de code redondant, sans forcer une implémentation spécifique. (Et rien ne vous empêche de transmettre des champs individuels à la fonction de validation, si vous n'aimez pas leFieldable
concept - mais maintenant, il y aurait au moins trois endroits dans lesquels la liste de champs devrait être maintenue.)