web-dev-qa-db-fra.com

Constructeur avec des tonnes de paramètres vs modèle de constructeur

Il est bien connu que si votre classe a un constructeur avec de nombreux paramètres, disons plus de 4, alors c'est très probablement un odeur de code . Vous devez reconsidérer si la classe satisfait SRP .

Mais que se passe-t-il si nous construisons et objectons cela dépend de 10 paramètres ou plus, et finissons par définir tous ces paramètres via le modèle Builder? Imaginez, vous construisez un objet de type Person avec ses informations personnelles, informations sur le travail, informations sur les amis, informations sur les intérêts, informations sur l'éducation, etc. C'est déjà bien, mais vous définissez en quelque sorte le même plus de 4 paramètres, non? Pourquoi ces deux cas ne sont-ils pas considérés comme identiques?

21
Narek

Le modèle de générateur ne résout pas le "problème" de nombreux arguments. Mais pourquoi de nombreux arguments posent-ils problème?

  • Ils indiquent que votre classe pourrait être en faire trop. Cependant, il existe de nombreux types qui contiennent légitimement de nombreux membres qui ne peuvent pas être raisonnablement regroupés.
  • Test et comprendre une fonction avec de nombreuses entrées devient exponentiellement plus compliqué - littéralement!
  • Lorsque le langage n'offre pas de paramètres nommés, un appel de fonction est non auto-documenté. La lecture d'un appel de fonction avec de nombreux arguments est assez difficile car vous n'avez aucune idée de ce que le 7ème paramètre est censé faire. Vous ne remarquerez même pas si les 5e et 6e arguments ont été échangés accidentellement, surtout si vous êtes dans un langage typé dynamiquement ou tout se trouve être une chaîne, ou lorsque le dernier paramètre est true pour une raison quelconque.

Falsification de paramètres nommés

Builder Pattern résout un seul de ces problèmes, à savoir les problèmes de maintenabilité des appels de fonction avec de nombreux arguments. Donc, un appel de fonction comme

MyClass o = new MyClass(a, b, c, d, e, f, g);

pourrait devenir

MyClass o = MyClass.builder()
  .a(a).b(b).c(c).d(d).e(e).f(f).g(g)
  .build();

∗ Le modèle Builder était à l'origine conçu comme une approche indépendante de la représentation pour assembler des objets composites, ce qui est une aspiration beaucoup plus grande que les arguments nommés pour les paramètres. En particulier, le modèle de générateur ne nécessite pas d'interface fluide.

Cela offre un peu de sécurité supplémentaire, car il explosera si vous appelez une méthode de générateur qui n'existe pas, mais cela ne vous apportera rien d'autre qu'un commentaire dans l'appel du constructeur n'aurait pas. De plus, la création manuelle d'un générateur nécessite du code, et plus de code peut toujours contenir plus de bogues.

Dans les langues où il est facile de définir un nouveau type de valeur, j'ai trouvé qu'il est préférable d'utiliser microtypage/minuscules types pour simuler des arguments nommés. Il est nommé ainsi parce que les types sont vraiment petits, mais vous finissez par taper beaucoup plus ;-)

MyClass o = new MyClass(
  new MyClass.A(a), new MyClass.B(b), new MyClass.C(c),
  new MyClass.D(d), new MyClass.E(e), new MyClass.F(f),
  new MyClass.G(g));

De toute évidence, les noms de type A, B, C,… doivent être des noms auto-documentés qui illustrent la signification du paramètre, souvent le même nom que vous donner la variable de paramètre. Par rapport à l'idiome du constructeur pour les arguments nommés, l'implémentation requise est beaucoup plus simple, et donc moins susceptible de contenir des bogues. Par exemple (avec la syntaxe Java-ish):

class MyClass {
  ...
  public static class A {
    public final int value;
    public A(int a) { value = a; }
  }
  ...
}

Le compilateur vous aide à garantir que tous les arguments ont été fournis; avec un générateur, vous devez vérifier manuellement les arguments manquants ou encoder une machine d'état dans le système de type de langage hôte - les deux contiendraient probablement des bogues.

Il existe une autre approche courante pour simuler des arguments nommés: un seul résumé objet paramètre qui utilise une syntaxe de classe en ligne pour initialiser tous les champs. En Java:

MyClass o = new MyClass(new MyClass.Arguments(){{ argA = a; argB = b; argC = c; ... }});

class MyClass {
  ...
  public static abstract class Arguments {
    public int argA;
    public String ArgB;
    ...
  }
}

Cependant, il est possible d'oublier les champs, et c'est une solution assez spécifique au langage (j'ai vu des utilisations en JavaScript, C # et C).

Heureusement, le constructeur peut toujours valider tous les arguments, ce qui n'est pas le cas lorsque vos objets sont créés dans un état partiellement construit, et demander à l'utilisateur de fournir d'autres arguments via des setters ou une méthode init() - ceux-ci nécessitent le moindre effort de codage, mais rend plus difficile l'écriture des programmes corrects .

Ainsi, bien qu'il existe de nombreuses approches pour résoudre les "nombreux paramètres sans nom rendent le code difficile à gérer", d'autres problèmes subsistent.

Aborder le problème racine

Par exemple le problème de testabilité. Lorsque j'écris des tests unitaires, j'ai besoin de pouvoir injecter des données de test et de fournir des implémentations de test pour simuler les dépendances et les opérations qui ont des effets secondaires externes. Je ne peux pas le faire lorsque vous instanciez des classes au sein de votre constructeur. À moins que la responsabilité de votre classe ne soit la création d'autres objets, elle ne devrait pas instancier de classes non triviales. Cela va de pair avec le problème de la responsabilité unique. Plus la responsabilité d'une classe est ciblée, plus elle est facile à tester (et souvent plus facile à utiliser).

L'approche la plus simple et souvent la meilleure consiste pour le constructeur à prendre des dépendances entièrement construites comme paramètre, bien que cela enlève la responsabilité de la gestion des dépendances à l'appelant - pas idéal non plus, sauf si les dépendances sont des entités indépendantes dans votre modèle de domaine.

Parfois sines (abstraites) ou cadres d'injection de dépendance complète sont utilisés à la place, bien que ceux-ci puissent être excessifs dans la majorité des cas d'utilisation. En particulier, ceux-ci ne réduisent le nombre d'arguments que si beaucoup de ces arguments sont des objets quasi-globaux ou des valeurs de configuration qui ne changent pas entre l'instanciation d'objet. Par exemple. si les paramètres a et d étaient globaux, nous obtiendrions

Dependencies deps = new Dependencies(a, d);
...
MyClass o = deps.newMyClass(b, c, e, f, g);

class MyClass {
  MyClass(Dependencies deps, B b, C c, E e, F f, G g) {
    this.depA = deps.newDepA(b, c);
    this.depB = deps.newDepB(e, f);
    this.g = g;
  }
  ...
}

class Dependencies {
  private A a;
  private D d;
  public Dependencies(A a, D d) { this.a = a; this.d = d; }
  public DepA newDepA(B b, C c) { return new DepA(a, b, c); }
  public DepB newDepB(E e, F f) { return new DepB(d, e, f); }
  public MyClass newMyClass(B b, C c, E e, F f, G g) {
    return new MyClass(deps, b, c, e, f, g);
  }
}

Selon l'application, cela peut changer la donne où les méthodes d'usine finissent par n'avoir presque aucun argument car tout peut être fourni par le gestionnaire de dépendances, ou cela peut être une grande quantité de code qui complique l'instanciation sans aucun avantage apparent. De telles usines sont bien plus utiles pour mapper des interfaces à des types concrets que pour gérer des paramètres. Cependant, cette approche essaie de résoudre le problème racine de trop de paramètres plutôt que de simplement le cacher avec une interface assez fluide.

25
amon

Le modèle de générateur ne résout rien pour vous et ne résout pas les échecs de conception.

Si vous avez une classe nécessitant 10 paramètres à construire, faire en sorte qu'un constructeur la construise n'améliorera pas soudainement votre conception. Vous devriez opter pour une refactorisation de la classe en question.

D'un autre côté, si vous avez une classe, peut-être un simple DTO, où certains attributs de la classe sont facultatifs, le modèle de générateur pourrait faciliter la construction dudit objet.

9
Andy

Chaque partie par ex. les informations personnelles, les informations sur le travail (chaque emploi "relais"), chaque ami, etc., doivent être converties en leurs propres objets.

Considérez comment vous implémenteriez une fonctionnalité de mise à jour. L'utilisateur veut ajouter un nouvel historique de travail. L'utilisateur ne veut modifier aucune autre partie des informations, ni l'historique de travail précédent - il suffit de les garder les mêmes.

Lorsqu'il y a beaucoup d'informations, il est inévitable de constituer les informations une par une. Vous pouvez regrouper ces éléments de façon logique - en fonction de l'intuition humaine (ce qui facilite leur utilisation pour un programmeur), ou en fonction du modèle d'utilisation (quels éléments d'information sont généralement mis à jour en même temps).

Le modèle de générateur est juste un moyen de capturer une liste (ordonnée ou non ordonnée) d'arguments typés, qui peuvent ensuite être passés dans le constructeur réel (ou le constructeur peut lire les arguments capturés du générateur).

La ligne directrice de 4 n'est qu'une règle de base. Il y a des situations qui nécessitent plus de 4 arguments et pour lesquelles il n'y a aucun moyen logique ou logique de les regrouper. Dans ces cas, il peut être judicieux de créer un struct qui peut être rempli directement ou avec des paramètres de propriété. Notez que le struct et le générateur dans ce cas ont un objectif très similaire.

Dans votre exemple, cependant, vous avez déjà décrit ce qui serait une façon logique de les regrouper. Si vous êtes confronté à une situation où ce n'est pas vrai, vous pouvez peut-être illustrer cela avec un exemple différent.

1
rwong