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

34

Le livre Effective Java de Joshua Bloch (2e édition) m'a beaucoup influencé, probablement plus que tout autre livre de programmation que j'ai lu. En particulier, son modèle de constructeur (élément 2) a eu le plus grand effet.

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

Il y a deux besoins principaux que j'ai, mais je voudrais seulement aborder le deuxième dans cette question:

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

  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?" Étendre une classe avec un constructeur est naturellement plus difficile que d’en étendre une sans. L'extension d'une classe ayant un générateur implémenté également Needable, auquel sont associés d'importants génériques , est donc difficile à manier.

Voici donc ma question: comment puis-je améliorer (ce que j'appelle) le constructeur Bloch, de sorte 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"? étendu et sous-étendu plusieurs fois - sans décourager mon avenir, ni les utilisateurs de ma bibliothèque , à cause du bagage supplémentaire que le constructeur (et ses génériques potentiels) leur impose?


Addendum
Ma question porte sur la partie 2 ci-dessus, mais je voudrais développer un peu le problème 1, y compris la façon dont je l’ai traitée:

Le premier problème est "comment partager des chaînes de méthodes auto-retournées, sans avoir à les ré-implémenter dans chaque ... unique ... classe?" Cela n'empêche pas les classes d'extension d'avoir à réimplémenter ces chaînes, ce qu'elles doivent bien sûr, mais plutôt comment empêcher les non-sous-classes , qui souhaitent tirer parti de ces chaînes de méthodes, -implémenter chaque fonction de retour automatique afin que leurs utilisateurs puissent en tirer parti? Pour cela, j'ai mis au point un modèle indispensable pour lequel je vais imprimer les squelettes d'interface ici et le laisser pour l'instant. Cela a bien fonctionné pour moi (cette conception prenait des années ... le plus difficile était 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);
}
aliteralmind
la source

Réponses:

21

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 ToBeBuiltclasse 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 classcontenu à 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:

  1. La ToBeBuiltclasse (dans cet exemple: UserConfig)
  2. Son Fieldableinterface " "
  3. Le constructeur

1. La classe à construire

La classe à construire accepte son Fieldableinterface 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 ToBeBuiltclasse 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 ToBeBuiltclasse 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 ToBeBuiltsont valides).

2. L' Fieldableinterface " "

L'interface pouvant être remplie est le "pont" entre la ToBeBuiltclasse et son constructeur, définissant tous les champs nécessaires à la construction de l'objet. Cette interface est requise par le ToBeBuiltconstructeur 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 ToBeBuiltclasse sans être obligée d'utiliser son générateur. Cela facilite également l’extension de la ToBeBuiltclasse, 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 Fieldableclasse. 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 ToBeBuiltconstructeur 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 ToBeBuiltconstructeur 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 ToBeBuiltclasse effectue le contrôle final.

Enfin, comme pour l' Fieldableinterface, 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 ToBeBuiltclasse,
  • 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:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

L' Fieldableinterface est entièrement optionnelle

Pour une ToBeBuiltclasse avec peu de champs obligatoires - comme cet UserConfigexemple 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 ToBeBuiltclasse est aussi "aveugle" (ignorant son constructeur) qu'elle l'est avec l' Fieldableinterface. Cependant, pour les ToBeBuiltclasses 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 ToBeBuiltconstructeur. 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 Fieldableclasses, pour tous les constructeurs aveugles, dans un sous-package de leur ToBeBuiltclasse. 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 ToBeBuiltconstructeur 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 ToBeBuiltclasse) 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 ToBeBuiltclasses seulement

Les accesseurs ne sont documentés que dans la ToBeBuiltclasse. Les accesseurs équivalents dans les classes _Fieldableet_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 @seeest 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 ToBeBuiltclasse , et aussi comme il fait la validation (qui est vraiment fait par le ToBeBuiltconstructeur 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));
   }
}

aliteralmind
la source
1
Décidément, c'est une amélioration. Le constructeur de Bloch, tel qu'il est mis en œuvre ici, couple deux classes concrètes , celle à construire et son constructeur. C'est une mauvaise conception en soi . Le constructeur aveugle que vous décrivez brise ce couplage en demandant à la classe à construire de définir sa dépendance de construction en tant qu'abstraction , que d'autres classes peuvent implémenter de manière découplée. Vous avez largement appliqué ce qui est une directive de conception essentielle orientée objet.
Rucamzu
3
Vous devriez vraiment bloguer à ce sujet quelque part si vous ne l'avez pas déjà fait, un bon morceau d'algorithme! Je suis partant pour le partager maintenant :-).
Martijn Verburg
4
Merci pour les mots gentils. Voici maintenant le premier article de mon nouveau blog: aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind
Si le générateur et les objets construits implémentent tous deux Fieldable, le modèle commence à ressembler à celui que j'ai appelé ReadableFoo / MutableFoo / ImmutableFoo, bien que, plutôt que de disposer de la méthode permettant de transformer une chose mutable en "membre" du constructeur, je appelez-le asImmutableet incluez-le dans l' ReadableFoointerface [en utilisant cette philosophie, l'appel buildd'un objet immuable renverrait simplement une référence au même objet].
Supercat
1
@ThomasN Vous devez étendre *_Fieldableet 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.
aliteralmind
13

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:

  1. Les objets ne doivent pas pouvoir être construits dans des états inconsistant / inutilisable / invalide.

    • Cela fait référence à des scénarios dans lesquels , par exemple , un Personobjet peut être construit sans l' avoir est Idrempli, alors que tous les morceaux de code que l' utilisation de cet objet peut exiger la Idjuste bien travailler avec le Person.
  2. 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:

//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 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 .

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 à 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:

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 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 .

Jimmy Hoffa
la source
Je ne pense pas que votre exemple donné satisfasse à l'une ou l'autre règle. Rien ne m'empêche de créer un Cfg dans un état non valide et, bien que les paramètres aient été déplacés du ctor, ils viennent tout juste d'être déplacés vers un endroit moins idiomatique et plus verbeux. 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.
Phoshi
Les propriétés des DTO peuvent être validées de nombreuses manières de manière déclarative avec des annotations, sur le poseur, mais vous voulez le faire - la validation est un problème distinct et, dans son approche de constructeur, il montre que la validation se produit dans le constructeur, cette même logique conviendrait parfaitement. dans mon approche. Cependant, il serait généralement préférable d’utiliser le DTO pour le valider car, comme je le dis, le DTO peut être utilisé pour construire plusieurs types, ce qui permettrait de valider plusieurs types. Le constructeur ne valide que le type pour lequel il est fait.
Jimmy Hoffa
Le moyen le plus flexible serait peut-être d’avoir une fonction de validation statique dans le générateur, qui accepte un seul Fieldableparamètre. Je qualifierais cette fonction de validation du ToBeBuiltconstructeur, 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 le Fieldableconcept - mais maintenant, il y aurait au moins trois endroits dans lesquels la liste de champs devrait être maintenue.)
aliteralmind
+1 Et une classe qui a trop de dépendances dans son constructeur n'est évidemment pas assez cohérente et devrait être refactorisée en classes plus petites.
Basilevs
@ JimmyHoffa: Ah, je vois, vous venez de l'omettre. Je ne suis pas sûr de voir la différence entre ceci et un générateur. En dehors de cela, une instance de configuration est transmise au ctor au lieu d'appeler .build sur un constructeur, et qu'un constructeur dispose d'un chemin plus évident pour la vérification de l'exactitude. les données. Chaque variable individuelle pourrait être dans ses plages valides, mais invalide dans cette permutation particulière. .build peut vérifier cela, mais transmettre l'élément dans le ctor nécessite une vérification d'erreur à l'intérieur de l'objet lui-même - icky!
Phoshi