web-dev-qa-db-fra.com

Comment améliorer le modèle de construction de Bloch, pour le rendre plus approprié pour une utilisation dans des classes hautement extensibles

J'ai été grandement influencé par le livre efficace de Joshua Bloch Java (2e édition), probablement plus que pour tout livre de programmation que j'ai lu. En particulier, son modèle de générateur (élément 2) a le plus grand effet.

Bien que le constructeur de Bloch m'ait beaucoup plus éloigné au cours des deux derniers mois que pendant mes dix dernières années de programmation, je me retrouve toujours à frapper le même mur: prolonger les cours avec des chaînes de méthodes à retour automatique est au mieux décourageant et au pire un cauchemar - en particulier lorsque des génériques entrent en jeu, et en particulier avec génériques autoréférentiels (comme Comparable<T extends Comparable<T>>).

Il y a deux besoins principaux que j'ai, seulement le deuxième sur lequel j'aimerais me concentrer dans cette question:

  1. Le premier problème est "comment partager des chaînes de méthodes à retour automatique, sans avoir à les ré-implémenter dans chaque ... seule ... classe?" Pour ceux qui peuvent être curieux, j'ai abordé cette partie au bas de ce message de réponse, mais ce n'est pas ce sur quoi je veux me concentrer ici.

  2. Le deuxième problème, sur lequel je demande des commentaires, est "comment puis-je implémenter un générateur dans des classes qui sont elles-mêmes destinées à être étendues par de nombreuses autres classes?" L'extension d'une classe avec un générateur est naturellement plus difficile que l'extension sans. L'extension d'une classe qui a un générateur qui implémente également Needable, et a donc des génériques significatifs qui lui sont associés , est lourde.

Voilà donc ma question: comment puis-je améliorer (ce que j'appelle) le Bloch Builder, afin que je puisse me sentir libre d'attacher un constructeur à n'importe quelle classe - même lorsque cette classe est censée être une "classe de base" qui peut être étendue et sous-étendue plusieurs fois - sans décourager mon avenir ni les utilisateurs de ma bibliothèque , en raison des bagages supplémentaires que le constructeur (et ses génériques potentiels) leur imposent?


Addendum
Ma question se concentre sur la partie 2 ci-dessus, mais je voulais développer un peu le problème un, y compris comment je l'ai traité:

Le premier problème est "comment partager des chaînes de méthodes à retour automatique, sans avoir à les ré-implémenter dans chaque ... seule ... classe?" Cela n'empêche pas les classes d'extension d'avoir à réimplémenter ces chaînes, ce qui, bien sûr, elles doivent - plutôt, comment empêcher non-sous-classes , qui veulent tirer parti de ces chaînes de méthodes, d'avoir à réimplémenter chaque fonction à retour automatique pour leurs utilisateurs pour pouvoir en profiter? Pour cela, j'ai mis au point un design nécessaire pour lequel j'imprimerai les squelettes d'interface ici, et je m'en tiendrai là pour l'instant. Cela a bien fonctionné pour moi (cette conception a duré des années ... la partie la plus difficile a été d'éviter les dépendances circulaires):

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

J'ai créé ce qui, pour moi, est une grande amélioration par rapport au Builder Pattern de Josh Bloch. Pour ne pas dire en aucune façon que c'est "mieux", juste que dans une situation très spécifique , cela offre certains avantages - le plus grand étant que il dissocie le générateur de sa classe à construire.

J'ai documenté en détail cette alternative ci-dessous, que j'appelle le modèle Blind Builder.


Modèle de conception: Blind Builder

Comme alternative à Joshua Bloch Builder Pattern (élément 2 dans Effective Java, 2nd edition), j'ai créé ce que j'appelle le "Blind Builder Pattern", qui partage de nombreux avantages du Bloch Builder et , mis à part un seul caractère, est utilisé exactement de la même manière. Les constructeurs aveugles ont l'avantage de

  • découpler le générateur de sa classe englobante, éliminant une dépendance circulaire,
  • réduit considérablement la taille du code source de (ce qui est plus maintenant) la classe englobante, et
  • permet d'étendre la classe ToBeBuilt sans avoir à étendre son constructeur.

Dans cette documentation, je ferai référence à la classe en cours de construction en tant que classe "ToBeBuilt".

Une classe implémentée avec un Bloch Builder

Un Bloch Builder est un public static class contenu dans la classe qu'il construit. Un exemple:

public class UserConfig {
 private final String sName; 
 private final int iAge; 
 private final String sFavColor; 
 public UserConfig (UserConfig.Cfg uc_c) {// CONSTRUCTEUR 
 //Transfert
 Essayez {
 SName = uc_c.sName; 
} Catch (NullPointerException rx) {
 Lève une nouvelle NullPointerException ("uc_c "); 
} 
 iAge = uc_c.iAge; 
 sFavColor = uc_c.sFavColor; 
 // VALIDER TOUS LES CHAMPS ICI 
} 
 public String toString () {
 return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor; 
} 
//builder...START
 public static class Cfg {
 private String sName; 
 private int iAge; 
 private String sFavColor; 
 public Cfg (String s_name) {
 SName = s_name; 
} 
 // se lf retournant des colons ... START 
 public Âge Cfg (int i_age) {
 iAge = i_age; 
 retourne ceci; 
} 
 public Cfg favoriteColor (String s_color) {
 SFavColor = s_color; 
 Renvoie ceci; 
} 
 // setters à retour automatique ... END 
 public UserConfig build () {
 return (new UserConfig (this)); 
} 
} 
 //builder...END
}

Instanciation d'une classe avec un Bloch Builder

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

La même classe, implémentée en tant que Blind Builder

Un Blind Builder se compose de trois parties, chacune étant dans un fichier de code source distinct:

  1. La classe ToBeBuilt (dans cet exemple: UserConfig)
  2. Son interface "Fieldable"
  3. Le constructeur

1. La classe à construire

La classe à construire accepte son interface Fieldable comme seul paramètre constructeur. Le constructeur en définit tous les champs internes, et valide chaque. Plus important encore, cette classe ToBeBuilt n'a aucune connaissance de son générateur.

public class UserConfig {
 private final String sName; 
 private final int iAge; 
 private final String sFavColor; 
 public UserConfig (UserConfig_Fieldable uc_f) {// CONSTRUCTOR 
 //transfer
 essayez {
 sName = uc_f.getName (); 
} catch (NullPointerException rx) {
 lever une nouvelle NullPointerException ("uc_f "); 
} 
 iAge = uc_f.getAge (); 
 sFavColor = uc_f.getFavoriteColor (); 
 // VALIDER TOUS LES CHAMPS ICI 
} 
 public String toString () {
 return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor; 
} 
}

Comme l'a noté un commentateur intelligent (qui a inexplicablement supprimé leur réponse), si la classe ToBeBuilt implémente également son Fieldable, son constructeur unique peut être utilisé à la fois comme son principal et constructeur de copie (un inconvénient est que les champs sont toujours validés, même s'il est connu que les champs du ToBeBuilt d'origine sont valides).

2. L'interface "Fieldable"

L'interface fieldable est le "pont" entre la classe ToBeBuilt et son générateur, définissant tous les champs nécessaires pour construire l'objet. Cette interface est requise par le constructeur de classes ToBeBuilt et est implémentée par le générateur. Puisque cette interface peut être implémentée par des classes autres que le générateur, n'importe quelle classe peut facilement instancier la classe ToBeBuilt, sans être forcée d'utiliser son générateur. Cela facilite également l'extension de la classe ToBeBuilt, lorsque l'extension de son générateur n'est ni souhaitable ni nécessaire.

Comme décrit dans une 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 générateur implémente la classe Fieldable. 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 que lorsque le constructeur de ToBeBuilt est appelé. Ceci est important, car il est possible pour qu'un autre thread manipule davantage le générateur, avant qu'il ne soit passé au constructeur de ToBeBuilt. La seule façon de garantir que les champs sont valides - en supposant que le générateur ne puisse pas "verrouiller" son état - est que la classe ToBeBuilt effectue la vérification finale.

Enfin, comme avec l'interface Fieldable, je ne documente aucun de ses getters.

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; 
} 
 // auto-déclarants ... START 
 public UserConfig_Cfg age (int i_age) {
 iAge = i_age; 
 renvoie ceci; 
} 
 public UserConfig_Cfg favoriteColor (String s_color) {
 sFavColor = s_color; 
 renvoie ceci; 
} 
 // setters à retour automatique ... END 
 //getters...START
 public String getName () {
 return sName; 
} 
 public int getAge () {
 return iAge; 
} 
 public String getFavoriteColor () {
 return sFavColor; 
} 
 //getters...END
 build UserConfig public () {
 return (nouveau UserConfig (this)); 
} 
}

Instanciation d'une classe avec un Blind Builder

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

La seule différence est "UserConfig_Cfg" au lieu de "UserConfig.Cfg "

Remarques

Désavantages:

  • Les constructeurs aveugles ne peuvent pas accéder aux membres privés de sa classe ToBeBuilt,
  • Ils sont plus verbeux, car des getters sont maintenant requis à la fois dans le générateur et dans l'interface.
  • Tout pour une seule classe n'est plus un seul endroit.

La compilation d'un Blind Builder est simple:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

L'interface Fieldable est entièrement facultative

Pour une classe ToBeBuilt avec quelques champs obligatoires - comme cette classe d'exemple UserConfig, le constructeur pourrait simplement être

userConfig public (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 complètement les getters (dans le générateur):

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

En passant directement des champs, la classe ToBeBuilt est tout aussi "aveugle" (ignorant son générateur) qu'elle l'est avec l'interface Fieldable. Cependant, pour les classes ToBeBuilt qui et sont destinées à être "étendues et sous-étendues plusieurs fois" (ce qui est dans le titre de cet article), toute modification de tout champ nécessite des changements de chaque sous-classe, dans chaque constructeur et ToBeBuilt constructeur. À mesure que le nombre de champs et de sous-classes augmente, cela devient difficile à maintenir.

(En effet, avec peu de champs nécessaires, utiliser un constructeur peut être excessif. Pour ceux qui sont intéressés, voici un échantillon de certaines des plus grandes interfaces Fieldable de ma bibliothèque personnelle.)

Classes secondaires en sous-ensemble

Je choisis d'avoir tous les constructeurs et les classes Fieldable, pour tous les constructeurs aveugles, dans un sous-package de leur classe ToBeBuilt. Le sous-package est toujours nommé "z". Cela empêche ces classes secondaires d'encombrer la liste des 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 se produit dans le constructeur de ToBeBuilt. Voici à nouveau le constructeur avec un exemple de code de validation:

userConfig public (UserConfig_Fieldable uc_f) {
 //transfer
 essayez {
 sName = uc_f.getName (); 
} catch (NullPointerException rx) {
 lève une nouvelle exception NullPointerException ("uc_f"); 
} 
 iAge = uc_f.getAge (); 
 sFavColor = uc_f.getFavoriteColor (); 
 // valider (devrait vraiment précompiler les modèles ...) 
 essayez {
 if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
 lever une nouvelle exception IllegalArgumentException ("uc_f.getName () (\" "+ sName +"\") peut ne pas être vide et ne doit contenir que des chiffres et des traits de soulignement."); 
} 
} catch (NullPointerException rx) {
 throw new NullPointerException ("uc_f.getName ()"); 
} 
 if (iAge <0) {
 lever une nouvelle exception IllegalArgumentException ("uc_f.getAge () (" + iAge + ") est inférieure à zéro."); 
} 
 essayez {
 if (! Pattern.compile ("(?: rouge | bleu | vert | rose vif)"). matcher (sFavColor) .matches ()) {
 lève une nouvelle exception IllegalArgumentException ("uc_f.getFavoriteColor () (\ "" + uc_f.getFavoriteColor () + "\") n'est ni rouge, ni bleu, ni vert, ni rose vif. "); 
} 
} catch (NullPointerException rx) {
 lancer une nouvelle NullPointerException ("uc_f.getFavoriteColor ()"); 
} 
}

Documentation des constructeurs

Cette section s'applique aux constructeurs Bloch et aux constructeurs aveugles. Il montre comment je documente les classes de cette conception, en créant des setters (dans le générateur) et leurs getters (dans la classe ToBeBuilt) directement croisés - avec un simple clic de souris, et sans l'utilisateur ayant besoin de savoir où résident réellement ces fonctions - et sans que le développeur ait à documenter quoi que ce soit de manière redondante.

Getters: dans les classes ToBeBuilt uniquement

Les accesseurs sont documentés uniquement dans la classe ToBeBuilt. Les getters équivalents dans les _Fieldable et _Cfg les classes sont ignorées. Je ne les documente pas du tout.

/**
 <P> L'âge de l'utilisateur. </P> 
 @Return Un entier représentant l'âge de l'utilisateur. 
 @See UserConfig_Cfg # age (int) 
 @see getName () 
 **/
 public int getAge () {
 return iAge; 
}

La première @see est un lien vers son setter, qui est dans la classe builder.

Setters: Dans la classe constructeur

Le passeur est documenté comme s'il était dans la classe ToBeBuilt, et aussi comme si il fait la validation (qui est vraiment effectuée par le constructeur de ToBeBuilt). L'astérisque ("* ") est un indice visuel indiquant que la cible du lien se trouve dans une autre classe.

/**}.____.] <P> Définissez 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; 
 retourner ceci; 
}

Plus d'informations

Tout mettre ensemble: La source complète de l'exemple Blind Builder, avec une documentation complète

UserConfig.Java

import 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 documentée uniquement dans les fonctions de définition du générateur. </P> 
 <P> {@code Java xbn.z.xmpl.lang.builder.finalv .UserConfig} </P> 
 **/
 Public class UserConfig {
 Public static final void main (String [] igno_red) {
 UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build (); 
 System.out.println (uc); 
} 
 Final privé String sName; 
 Private final int iAge; 
 Private final String sFavColor; 
 /**
 <P> Créez 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) {
 // transfer 
 essayez {
 sName = uc_f.getName (); 
} catch (NullPointerException rx) {
 lancez une nouvelle NullPointerException ("uc_f"); 
} 
 iAge = uc_f.getAge (); 
 sFavColor = uc_f.getFavoriteColor (); 
 //validation
 essayez {
 if (! Pattern.compile ( "\\ w +"). matcher (sName) .matches ()) {
 lever une nouvelle exception IllegalArgumentException ("uc_f.getName () (\" "+ sName +"\") peut ne pas être vide et doit ne contient que des chiffres et des traits de soulignement. "); 
} 
} catch (NullPointerException rx) {
 lever une nouvelle NullPointerException (" uc_f.getName () "); 
} 
 if (iAge <0) {
 lever une nouvelle exception IllegalArgumentException ("uc_f.getAge () (" + iAge + ") est inférieure à zéro."); 
} 
 essayez {
 si (! Pattern.compile ("(?: rouge | bleu | vert | rose vif)"). matcher (sFavColor) .matches ()) {
 lever une nouvelle exception IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +"\") n'est pas rouge, bleu, vert ou rose vif."); 
 } 
} catch (NullPointerException rx) {
 lever une nouvelle NullPointerException ("uc_f.getFavoriteColor ()"); 
} 
} 
 // getters ... START 
 /**
 <P> Le nom de l'utilisateur. </P> 
 @return Une chaîne non - {@ code null}, non vide. 
 @see UserConfig_Cfg # UserConfig_Cfg (String) 
 @see #getAge () 
 @see #getFavoriteColor () 
 **/
 String public getName () {
 return sName; 
} 
 /**
 <P> L'âge de l'utilisateur. </P> 
 @return A nombre supérieur ou égal à zéro. 
 @see UserConfig_Cfg # age (int) 
 @see #getName () 
 **/
 public int getAge () {
 return iAge; 
} 
 /**
 <P> La couleur préférée de l'utilisateur. </P> 
 @ return Une chaîne non - {@ code null}, non vide. 
 @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> Requis par l'interface publique {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable)}. </P> 
 **/
 Interface publique UserConfig_Fieldable {
 String getName (); 
 Int getAge (); 
 String getFavoriteColor (); 
}

UserConfig_Cfg.Java

import Java.util.regex.Pattern; 
/** 
 <P> Builder for {@link UserConfig}. </P> 
 <P> La validation de tous les champs se produit dans le constructeur <CODE> UserConfig </CODE>. Cependant, chaque exigence de validation est documentée uniquement dans les fonctions de définition de cette classe. </P> 
 **/
 Classe publique UserConfig_Cfg implémente UserConfig_Fieldable {
 Public String sName; 
 public int iAge; 
 public String sFavColor; 
 /**
 <P> Créez une nouvelle instance avec le nom de l'utilisateur. </P> 
 @param s_name Peut ne pas être {@code null} ou vide, et ne doit contenir que 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; 
} 
 // régleurs à retour automatique ... START 
 /**
 <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; 
 retourne 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 rose"}. Obtenez avec {@code UserConfig # getName () getName ()} {@ code ()} *. 
 @See #age (int) 
 **/
 Public UserConfig_Cfg favoriteColor (String s_color) {
 SFavColor = s_color; 
 Retourne ceci; 
} 
 // paramètres de retour automatique ... FIN 
 // getters ... START 
 public String getName () {
 return sName; 
} 
 public int getAge () {
 return iAge; 
} 
 public String getFavoriteColor () {
 return sFavColor; 
} 
 //getters...END
/** 
 <P> Construisez UserConfig, tel que configuré. </P> 
 @Return <CODE> (nouveau {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (this)) </CODE> 
 **/
 public UserConfig build () {
 return (new UserConfig (this)); 
} 
}
21
aliteralmind

Je pense que la question ici suppose d'emblée quelque chose sans essayer de le prouver, que le modèle de constructeur est intrinsèquement bon.

tl; dr je pense que le modèle de constructeur est rarement si jamais une bonne idée.


Objectif du modèle de générateur

L'objectif du modèle de générateur est de maintenir deux règles qui faciliteront la consommation de votre classe:

  1. Les objets ne doivent pas pouvoir être construits dans des états incohérents/inutilisables/invalides.

    • Cela fait référence à des scénarios où, par exemple, un objet Person peut être construit sans avoir son Id rempli, tandis que tous les morceaux de code qui utilisent cet objet peuvent exiger le Id juste pour fonctionner correctement avec le Person.
  2. Les constructeurs d'objets ne devraient pas exiger trop de paramètres .

Le but du modèle de générateur est donc non controversé. Je pense qu'une grande partie du désir et de l'utilisation de celui-ci est basée sur une analyse qui est essentiellement allée aussi loin: nous voulons ces deux règles, cela donne ces deux règles - même si je pense qu'il vaut la peine d'étudier d'autres façons d'accomplir ces deux règles.


Pourquoi s'embêter à envisager d'autres approches?

Je pense que la raison est bien illustrée par le fait de cette question elle-même; il y a de la complexité et beaucoup de cérémonie ajoutée aux structures en leur appliquant le modèle de constructeur. Cette question demande comment résoudre une partie de cette complexité car, comme le fait souvent la complexité, elle crée un scénario qui se comporte étrangement (héritant). Cette complexité augmente également les frais de maintenance (l'ajout, la modification ou la suppression de propriétés est beaucoup plus complexe qu'autrement).


D'autres approches

Donc, pour la règle numéro un ci-dessus, quelles sont les approches? La clé à laquelle cette règle fait référence est que lors de la construction, un objet possède toutes les informations dont il a besoin pour fonctionner correctement - et après la construction, ces informations ne peuvent pas être modifiées en externe (il s'agit donc d'informations 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 ces informations sont demandées 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 c'est le cas cette approche violerait la règle # 2 ci-dessus .

OK quoi d'autre est là? 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 qui est pris au moment de la construction. Votre code ci-dessus au lieu d'avoir un modèle de générateur serait alors:

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

Ce n'est pas très différent du modèle de générateur, bien qu'il soit légèrement plus simple, et surtout nous satisfaisons maintenant la règle # 1 et la règle # 2 .

Alors pourquoi ne pas aller un peu plus loin et en faire un constructeur complet? C'est tout simplement inutile . J'ai satisfait les deux objectifs du modèle de générateur dans cette approche, avec quelque chose d'un peu plus simple, plus facile à entretenir, et réutilisable. Ce dernier bit est essentiel, cet exemple utilisé est imaginaire et ne se prête pas à un objectif sémantique réel, alors montrons comment cette approche se traduit par un DTO réutilisable plutôt qu'une classe à usage unique =.

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

Ainsi, lorsque vous créez des DTO cohésifs comme celui-ci, ils peuvent tous deux satisfaire l'objectif du modèle de générateur, plus simplement et avec une valeur/utilité plus large. De plus, cette approche résout la complexité d'héritage que le modèle de générateur entraîne:

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

Vous pouvez trouver que le DTO n'est pas toujours cohérent, ou pour rendre les regroupements de propriétés cohérents, ils doivent être répartis sur 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 DTO cohésifs avec ces propriétés, vous avez une construction simple qui répond aux objectifs des constructeurs, puis certaines. Si vous ne pouvez pas proposer de regroupements cohérents, cela peut être un signe que vos objets ne sont pas cohérents s'ils ont des propriétés qui ne sont pas du tout liées - mais même dans ce cas, la création d'un DTO non cohésif unique est toujours préférable en raison de la mise en œuvre plus simple, plus résoudre votre problème d'héritage.


Comment améliorer le modèle de générateur

Ok donc toutes les randonnées de bordures de côté, vous avez un problème et cherchez une approche de conception pour le résoudre. Ma suggestion: l'héritage des classes peut simplement avoir une classe imbriquée qui hérite de la classe de génération de la super classe, donc la classe d'héritage a fondamentalement la même structure que la super classe et a un modèle de générateur qui devrait fonctionner exactement de la même manière avec les fonctions supplémentaires pour les propriétés supplémentaires de la sous-classe ..


Quand c'est une bonne idée

Rantant de côté, le modèle de constructeur a une niche. Nous le savons tous parce que nous avons tous appris ce générateur 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 générateur car il présente un avantage en termes de performances.

L'avantage de performance est donc: vous avez un tas d'objets, ils sont d'un type immuable, vous devez les réduire en un seul objet d'un type immuable. Si vous le faites de manière incrémentielle, vous créerez ici de nombreux objets intermédiaires, donc le faire à la fois est beaucoup plus performant et idéal.

Donc je pense que la clé de quand c'est une bonne idée est dans le domaine problématique du StringBuilder: Besoin de transformer plusieurs instances de types immuables en une seule instance d'un type immuable .

13
Jimmy Hoffa