Question

Supposons que vous ayez une classe appelée Customer, qui contient les champs suivants :

  • Nom d'utilisateur
  • E-mail
  • Prénom
  • Nom de famille

Disons également que selon votre logique métier, tous les objets Customer 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 comprendre comment cela peut devenir incontrôlable lorsque vous êtes obligé d'ajouter davantage de champs obligatoires à l'objet Customer.

J'ai vu des classes qui intègrent plus de 20 arguments dans leur constructeur et c'est juste pénible de les utiliser.Mais, alternativement, si vous n'avez pas besoin de ces champs, vous courez le risque d'avoir des informations non définies, ou pire, des erreurs de référencement d'objet si vous comptez sur le code appelant pour spécifier ces propriétés.

Existe-t-il des alternatives à cela ou devez-vous simplement décider si le nombre X d'arguments du constructeur est trop important pour vous ?

Était-ce utile?

La solution

Deux approches de conception à considérer

Le essence modèle

Le interface fluide modèle

Ces deux objectifs ont une intention similaire, dans la mesure où nous construisons lentement un objet intermédiaire, puis créons notre objet cible en une seule étape.

Un exemple d’interface fluide en action serait :

public class CustomerBuilder {
    String surname;
    String firstName;
    String ssn;
    public static CustomerBuilder customer() {
        return new CustomerBuilder();
    }
    public CustomerBuilder withSurname(String surname) {
        this.surname = surname; 
        return this; 
    }
    public CustomerBuilder withFirstName(String firstName) {
        this.firstName = firstName;
        return this; 
    }
    public CustomerBuilder withSsn(String 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 String firstName;
    private final String surname;
    private final String 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 String getFirstName() { return firstName;  }
    public String getSurname() { return surname; }
    public String 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();
    }
}

Autres conseils

Je vois que certaines personnes recommandent sept comme limite supérieure.Apparemment, il n’est pas vrai que les gens peuvent avoir sept choses en tête à la fois ;ils ne se souviennent que de quatre (Susan Weinschenk, 100 choses que tout designer doit savoir sur les gens, 48).Malgré tout, je considère que quatre correspond à une orbite terrestre élevée.Mais c'est parce que ma pensée a été modifiée par Bob Martin.

Dans Code propre, Oncle Bob plaide pour trois comme limite supérieure générale du nombre de paramètres.Il fait cette affirmation radicale (40) :

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

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

Imaginez la difficulté d'écrire tous les cas de test pour garantir que toutes les différentes combinaisons d'arguments fonctionnent correctement.

Je vous encourage à trouver un exemplaire de son livre et à lire sa discussion complète sur les arguments des fonctions (40-43).

Je suis d'accord avec ceux qui ont évoqué le principe de responsabilité unique.Il m'est difficile de croire qu'une classe qui a besoin de plus de deux ou trois valeurs/objets sans valeurs par défaut raisonnables n'a en réalité qu'une seule responsabilité et ne serait pas mieux lotie avec une autre classe extraite.

Maintenant, si vous injectez vos dépendances via le constructeur, les arguments de Bob Martin sur la facilité avec laquelle il est d'invoquer le constructeur ne s'appliquent pas vraiment (car généralement, il n'y a qu'un seul point dans votre application où vous câblez cela, ou vous même avez un framework qui le fait pour vous).Cependant, le principe de responsabilité unique reste toujours d’actualité :une fois qu'une classe a quatre dépendances, je considère que cela sent qu'elle fait une grande quantité de travail.

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

Dans votre cas, restez fidèle au constructeur.Les informations appartiennent à Client et 4 champs conviennent.

Dans le cas où vous disposez de nombreux champs obligatoires et facultatifs, le constructeur n’est pas la meilleure solution.Comme l'a dit @boojiboy, 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 setters par défaut pour les attributs 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 constructeur.Un exemple tiré du livre :

 public class NutritionFacts {  
   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 NutritionFacts build() {  
       return new NutritionFacts(this);  
     }  
   }  

   private NutritionFacts(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 :

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

L'exemple ci-dessus est tiré de Java 2 efficace

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

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

Rendre le rectangle explicite en tant qu'objet explique mieux le code :

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

Je pense que la réponse "pure POO" est que si les opérations sur la classe ne sont pas valides lorsque certains membres ne sont pas initialisés, alors ces membres doivent être définis par le constructeur.Il existe toujours un 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 constructeur unique autorisé une fois l'API rendue publique 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 la seule façon de gérer la situation.En particulier avec les objets WPF, vous constaterez que les classes .NET ont tendance à favoriser les constructeurs sans paramètre et lèveront des exceptions si les données n'ont pas été initialisées à un état souhaité avant d'appeler la méthode.Ceci est probablement principalement spécifique à la conception basée sur les composants ;Je ne parviens 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 l'approche « le constructeur définit les propriétés requises » si votre API est gravée dans le marbre ou n'est pas publique.

La seule chose que je suis Ce qui est sûr, c’est qu’il existe probablement d’innombrables méthodologies capables de résoudre ce problème, et chacune d’elles introduit son propre ensemble de problèmes.La meilleure chose à faire est d’apprendre autant de modèles que possible et de choisir celui qui convient le mieux au travail.(N'est-ce pas une telle réponse dérobée ?)

Je pense que votre question concerne davantage la conception de vos classes que le nombre d'arguments dans le constructeur.Si j'avais besoin de 20 éléments de données (arguments) pour initialiser avec succès un objet, j'envisagerais probablement de diviser la classe.

Steve McConnell écrit dans Code Complete que les gens ont du mal à garder plus de 7 choses en tête à la fois, ce serait donc le nombre sous lequel j'essaie de rester.

Si vous avez de nombreux arguments désagréables, regroupez-les simplement dans des classes structs / POD, de préférence déclarées comme 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.

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

Je pense que le moyen le plus simple serait de trouver une valeur par défaut acceptable pour chaque valeur.Dans ce cas, chaque champ semble devoir être construit, donc éventuellement surcharger l'appel de fonction afin que si quelque chose n'est pas défini dans l'appel, le définir par défaut.

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

Implémentation Java :

public static void setEmail(String newEmail){
    this.email = newEmail;
}

public static String getEmail(){
    return this.email;
}

C'est également une bonne pratique pour sécuriser vos variables globales.

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

Je suis d'accord sur la limite de 7 articles 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 la clé primaire vers une autre source de données.

J'encapsulerais des champs similaires dans un objet qui lui est propre avec sa propre logique de construction/validation.

Disons par exemple, si vous avez

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

Je créerais une classe qui stocke le téléphone et l'adresse avec une balise spécifiant s'il s'agit d'un téléphone/adresse « domicile » ou « professionnel ».Et puis réduisez les 4 champs à un simple 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 faire moins ressembler à des spaghettis.

Si vous avez beaucoup de champs, vous devez sûrement pouvoir extraire un modèle qui constituerait une belle unité de fonction.Et créez également un code plus lisible.

Et voici également des solutions possibles :

  • Étalez la logique de validation au lieu de la stocker dans une seule classe.Validez lorsque l'utilisateur les saisit, puis validez à nouveau au niveau de la couche de base de données, etc.
  • Faire un CustomerFactory cours qui m'aiderait à construire Customers
  • La solution de @marcio est également intéressante...

Utilisez simplement les arguments par défaut.Dans un langage qui prend 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 langages 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é.

Cela dépend simplement de savoir s'il est approprié ou non que vous définissiez ces valeurs par défaut, ou si vos objets doivent être spécifiés à tout moment lors de la construction.C'est vraiment une décision que vous seul pouvez prendre.

À moins qu'il n'y ait plus d'un argument, j'utilise toujours des tableaux ou des objets comme paramètres de constructeur et je m'appuie sur la vérification des erreurs pour m'assurer que les paramètres requis sont là.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top