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
true
pour 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 a
et 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.