Combien d’arguments de constructeur sont trop nombreux?

Disons que vous avez une classe appelée Customer, qui contient les champs suivants:

  • Nom d’utilisateur
  • Email
  • Prénom
  • Nom de famille

Disons également que selon votre logique métier, tous les objects Client doivent avoir ces quatre propriétés définies.

Maintenant, nous pouvons le faire assez facilement en forçant le constructeur à spécifier chacune de ces propriétés. Mais il est assez facile de voir comment cela peut dégénérer lorsque vous êtes obligé d’append d’autres champs obligatoires à l’object Client.

J’ai vu des classes qui intègrent plus de 20 arguments dans leur constructeur et c’est juste pénible de les utiliser. Cependant, si vous n’avez pas besoin de ces champs, vous risquez d’avoir des informations indéfinies, ou pire, des erreurs de référencement d’object si vous utilisez le code d’appel pour spécifier ces propriétés.

Y a-t-il des alternatives à cela ou devez-vous simplement décider si X quantité d’arguments de constructeur sont trop nombreux pour que vous puissiez les utiliser?

    Deux approches de conception à considérer

    Le motif de l’ essence

    Le modèle d’ interface courant

    Ils ont tous deux une intention similaire, en ce sens que nous construisons lentement un object intermédiaire, puis créons notre object cible en une seule étape.

    Un exemple de l’interface fluide en action serait:

    public class CustomerBuilder { Ssortingng surname; Ssortingng firstName; Ssortingng ssn; public static CustomerBuilder customer() { return new CustomerBuilder(); } public CustomerBuilder withSurname(Ssortingng surname) { this.surname = surname; return this; } public CustomerBuilder withFirstName(Ssortingng firstName) { this.firstName = firstName; return this; } public CustomerBuilder withSsn(Ssortingng ssn) { this.ssn = ssn; return this; } // client doesn't get to instantiate Customer directly public Customer build() { return new Customer(this); } } public class Customer { private final Ssortingng firstName; private final Ssortingng surname; private final Ssortingng ssn; Customer(CustomerBuilder builder) { if (builder.firstName == null) throw new NullPointerException("firstName"); if (builder.surname == null) throw new NullPointerException("surname"); if (builder.ssn == null) throw new NullPointerException("ssn"); this.firstName = builder.firstName; this.surname = builder.surname; this.ssn = builder.ssn; } public Ssortingng getFirstName() { return firstName; } public Ssortingng getSurname() { return surname; } public Ssortingng getSsn() { return ssn; } } import static com.acme.CustomerBuilder.customer; public class Client { public void doSomething() { Customer customer = customer() .withSurname("Smith") .withFirstName("Fred") .withSsn("123XS1") .build(); } } 

    Je vois que certaines personnes recommandent sept comme limite supérieure. Apparemment, ce n’est pas vrai que les gens peuvent tenir sept choses dans leur tête à la fois; ils ne peuvent en retenir que quatre (Susan Weinschenk, 100 choses que tout concepteur doit savoir sur les gens , 48). Malgré cela, je considère quatre comme une orbite terrestre élevée. Mais c’est parce que ma reflection a été modifiée par Bob Martin.

    Dans Clean Code , Uncle Bob préconise trois comme limite supérieure générale pour le nombre de parameters. Il fait la revendication radicale (40):

    Le nombre idéal d’arguments pour une fonction est zéro (niladique). Vient ensuite l’un (monadique) suivi de près par deux (dyadique). Trois arguments (sortingadiques) doivent être évités autant que possible. Plus de trois (polyadiques) nécessitent une justification très particulière et ne devraient pas être utilisés de toute façon.

    Il dit cela à cause de la lisibilité; mais aussi à cause de la testabilité:

    Imaginez la difficulté d’écrire tous les cas de test pour vous assurer que toutes les combinaisons d’arguments fonctionnent correctement.

    Je vous encourage à trouver une copie de son livre et à lire sa discussion complète sur les arguments fonctionnels (40-43).

    Je suis d’accord avec ceux qui ont mentionné le principe de la responsabilité unique. Il m’est difficile de croire qu’une classe qui a besoin de plus de deux ou trois valeurs / objects sans valeurs par défaut raisonnables n’a vraiment qu’une seule responsabilité et ne serait pas mieux avec une autre classe extraite.

    Maintenant, si vous injectez vos dépendances à travers le constructeur, les arguments de Bob Martin sur la facilité d’appeler le constructeur ne s’appliquent pas vraiment (parce qu’il ya généralement un seul point dans votre application où avoir un cadre qui le fait pour vous). Cependant, le principe de la responsabilité unique est toujours d’actualité: une fois qu’une classe a quatre dépendances, je considère que c’est une odeur de travail considérable.

    Cependant, comme pour tout ce qui concerne l’informatique, il existe sans aucun doute des cas valables pour avoir un grand nombre de parameters de constructeur. Ne contorssez pas votre code pour éviter d’utiliser un grand nombre de parameters. mais si vous utilisez un grand nombre de parameters, arrêtez-vous et réfléchissez-y, car cela peut signifier que votre code est déjà déformé.

    Dans votre cas, restz avec le constructeur. L’information appartient au client et 4 champs sont corrects.

    Si vous avez plusieurs champs obligatoires et facultatifs, le constructeur n’est pas la meilleure solution. Comme @boojiboy l’a dit, c’est difficile à lire et il est également difficile d’écrire du code client.

    @contagious a suggéré d’utiliser le modèle et les parameters par défaut pour les atsortingbuts facultatifs. Cela exige que les champs soient mutables, mais c’est un problème mineur.

    Joshua Block sur Effective Java 2 dit que dans ce cas, vous devriez envisager un générateur. Un exemple tiré du livre:

      public class NusortingtionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public static class Builder { // required parameters private final int servingSize; private final int servings; // optional parameters private int calories = 0; private int fat = 0; private int carbohydrate = 0; private int sodium = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public NusortingtionFacts build() { return new NusortingtionFacts(this); } } private NusortingtionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; soduim = builder.sodium; carbohydrate = builder.carbohydrate; } } 

    Et puis utilisez-le comme ceci:

     NusortingtionFacts cocaCola = new NusortingtionFacts.Builder(240, 8). calories(100).sodium(35).carbohydrate(27).build(); 

    L’exemple ci-dessus a été tiré de Java effectif 2

    Et cela ne s’applique pas uniquement au constructeur. Citant Kent Beck dans les modèles de mise en œuvre :

     setOuterBounds(x, y, width, height); setInnerBounds(x + 2, y + 2, width - 4, height - 4); 

    Rendre le rectangle explicite comme un object explique mieux le code:

     setOuterBounds(bounds); setInnerBounds(bounds.expand(-2)); 

    Je pense que votre question concerne plus la conception de vos classes que le nombre d’arguments dans le constructeur. Si j’avais besoin de 20 données (arguments) pour initialiser un object avec succès, je penserais probablement à diviser la classe.

    Je pense que la réponse “pure OOP” est que si les opérations sur la classe sont invalides quand certains membres ne sont pas initialisés, alors ces membres doivent être définis par le constructeur. Il y a toujours le cas où les valeurs par défaut peuvent être utilisées, mais je suppose que nous n’envisageons pas ce cas. C’est une bonne approche lorsque l’API est corrigée, car changer le seul constructeur autorisé après la publication de l’API sera un cauchemar pour vous et tous les utilisateurs de votre code.

    En C #, ce que je comprends des directives de conception, c’est que ce n’est pas nécessairement le seul moyen de gérer la situation. En particulier avec les objects WPF, vous constaterez que les classes .NET ont tendance à privilégier les constructeurs sans paramètre et lanceront des exceptions si les données n’ont pas été initialisées à un état souhaitable avant d’appeler la méthode. Ceci est probablement principalement spécifique à la conception basée sur les composants; Je ne peux pas trouver un exemple concret d’une classe .NET qui se comporte de cette manière. Dans votre cas, cela entraînerait certainement une charge de test accrue pour garantir que la classe n’est jamais enregistrée dans le magasin de données, à moins que les propriétés n’aient été validées. Honnêtement, à cause de cela, je préférerais que l’approche “constructeur définit les propriétés requirejses” si votre API est définie dans la pierre ou non.

    La seule chose dont je suis certain, c’est qu’il ya probablement d’innombrables méthodologies qui peuvent résoudre ce problème, et chacune d’elles présente ses propres problèmes. La meilleure chose à faire est d’apprendre autant de modèles que possible et de choisir le meilleur pour le travail. (N’est-ce pas une échappatoire à une réponse?)

    Si vous avez beaucoup d’arguments, placez-les simplement dans des classes structs / POD, de préférence déclarées en tant que classes internes de la classe que vous construisez. De cette façon, vous pouvez toujours exiger les champs tout en rendant le code qui appelle le constructeur raisonnablement lisible.

    Steve Mcconnell écrit dans Code Complete que les gens ont du mal à garder plus de choses dans leur tête à la fois, alors ce serait le nombre que j’essaierais de garder.

    Je pense que tout dépend de la situation. Pour quelque chose comme votre exemple, une classe de client, je ne risquerais pas de voir ces données indéfinies en cas de besoin. D’un autre côté, passer une structure effacerait la liste des arguments, mais vous auriez encore beaucoup de choses à définir dans la structure.

    Je pense que le plus simple serait de trouver un défaut acceptable pour chaque valeur. Dans ce cas, chaque champ ressemble à ce qu’il devrait être construit, ainsi, il est possible de surcharger l’appel de fonction pour que, si quelque chose n’est pas défini dans l’appel, il soit défini par défaut.

    Ensuite, créez des fonctions de lecture et de définition pour chaque propriété afin que les valeurs par défaut puissent être modifiées.

    Implémentation Java:

     public static void setEmail(Ssortingng newEmail){ this.email = newEmail; } public static Ssortingng getEmail(){ return this.email; } 

    C’est également une bonne pratique pour garder vos variables globales sécurisées.

    Le style compte beaucoup, et il me semble que s’il y a un constructeur avec plus de 20 arguments, la conception doit être modifiée. Fournir des valeurs par défaut raisonnables.

    J’encapsulerais des champs similaires dans un object propre avec sa propre logique de construction / validation.

    Dites par exemple, si vous avez

    • Téléphone de travail
    • Adresse d’affaires
    • Téléphone fixe
    • Adresse du domicile

    Je ferais une classe qui stocke le téléphone et l’adresse avec une balise spécifiant si c’est une adresse / un téléphone “personnel” ou “professionnel”. Et puis réduisez les 4 champs simplement en un tableau.

     ContactInfo cinfos = new ContactInfo[] { new ContactInfo("home", "+123456789", "123 ABC Avenue"), new ContactInfo("biz", "+987654321", "789 ZYX Avenue") }; Customer c = new Customer("john", "doe", cinfos); 

    Cela devrait ressembler moins à des spaghettis.

    Bien sûr, si vous avez beaucoup de champs, il doit y avoir un modèle que vous pouvez extraire pour créer une bonne unité de fonction. Et pour un code plus lisible.

    Et les solutions suivantes sont également possibles:

    • Étalez la logique de validation au lieu de la stocker dans une seule classe. Validez lorsque l’utilisateur les saisit, puis validez à nouveau dans la couche de firebase database, etc.
    • Créer une classe CustomerFactory qui m’aiderait à construire des Customer
    • La solution de @ marcio est également intéressante …

    À moins que ce soit plus d’un argument, j’utilise toujours des tableaux ou des objects en tant que parameters de constructeur et je m’appuie sur la vérification des erreurs pour m’assurer que les parameters requirejs sont présents.

    Utilisez simplement les arguments par défaut. Dans un langage prenant en charge les arguments de méthode par défaut (PHP, par exemple), vous pouvez le faire dans la signature de la méthode:

    public function doSomethingWith($this = val1, $this = val2, $this = val3)

    Il existe d’autres moyens de créer des valeurs par défaut, par exemple dans les langues prenant en charge la surcharge de méthodes.

    Bien entendu, vous pouvez également définir des valeurs par défaut lorsque vous déclarez les champs, si vous le jugez approprié.

    Il ne s’agit que de savoir s’il est approprié ou non de définir ces valeurs par défaut ou si vos objects doivent toujours être spécifiés lors de la construction. C’est vraiment une décision que vous seul pouvez prendre.

    Je suis d’accord sur la limite de 7 points mentionnée par Boojiboy. Au-delà de cela, il peut être intéressant d’examiner les types anonymes (ou spécialisés), IDictionary ou l’indirection via une clé primaire vers une autre source de données.