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

21

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 une 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 Personobjet type 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?

Narek
la source

Réponses:

25

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 en fait trop . Cependant, il existe de nombreux types qui contiennent légitimement de nombreux membres qui ne peuvent pas être raisonnablement regroupés.
  • Tester 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 n'est pas 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 truepour une raison quelconque.

Falsification de paramètres nommés

Le modèle de générateur ne traite que l'un 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 était préférable d'utiliser des types de microtypage / minuscules 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 autodocumenté qui illustrent la signification du paramètre, souvent le même nom que vous souhaitez donner la variable de paramètres. 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 contiennent probablement des bogues.

Il existe une autre approche courante pour simuler des arguments nommés: un seul objet de paramètre abstrait 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 obliger l'utilisateur à fournir d'autres arguments via des setters ou une init()méthode - ceux-ci nécessitent le moins d'effort de codage, mais font il est plus difficile d'écrire 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 le problème», d'autres problèmes demeurent.

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 il est facile de 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, des usines (abstraites) ou des 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 aet détaient-ish mondiale, 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.

amon
la source
Je plaiderais vraiment contre la partie auto-documentée. Si vous avez un IDE décent + des commentaires décents, la plupart du temps, le constructeur et la définition des paramètres sont à une touche.
JavierIEH
Dans Android Studio 3.0, le nom du paramètre dans le constructeur est affiché à côté de la valeur transmise dans un appel de constructeur. par exemple: nouveau A (opérande1: 34, opérande2: 56); l'opérande1 et l'opérande2 sont les noms de paramètres dans le constructeur. Ils sont montrés par l'IDE pour rendre le code plus lisible. Il n'est donc pas nécessaire d'aller à la définition pour savoir quel est le paramètre.
grenat
9

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.

Andy
la source
1

Chaque partie, par exemple 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 souhaite 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'existe aucun moyen logique ou logique de les regrouper. Dans ces cas, il peut être judicieux de créer un structqui peut être rempli directement ou avec des évaluateurs de propriétés. Notez que lestruct 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.

rwong
la source