Les accesseurs et modificateurs (alias setters et getters) sont utiles pour trois raisons principales:
- Ils restreignent l'accès aux variables.
- Par exemple, une variable est accessible, mais pas modifiée.
- Ils valident les paramètres.
- Ils peuvent provoquer certains effets secondaires.
Les universités, les cours en ligne, les didacticiels, les articles de blog et les exemples de code sur le Web soulignent tous l'importance des accesseurs et des modificateurs, ils se sentent presque comme un "must have" pour le code de nos jours. On peut donc les trouver même s'ils ne fournissent aucune valeur supplémentaire, comme le code ci-dessous.
public class Cat {
private int age;
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
Cela dit, il est très courant de trouver des modificateurs plus utiles, ceux qui valident réellement les paramètres et lèvent une exception ou retournent un booléen si une entrée non valide a été fournie, quelque chose comme ceci:
/**
* Sets the age for the current cat
* @param age an integer with the valid values between 0 and 25
* @return true if value has been assigned and false if the parameter is invalid
*/
public boolean setAge(int age) {
//Validate your parameters, valid age for a cat is between 0 and 25 years
if(age > 0 && age < 25) {
this.age = age;
return true;
}
return false;
}
Mais même alors, je ne vois presque jamais les modificateurs appelés depuis un constructeur, donc l'exemple le plus courant d'une classe simple avec laquelle je suis confronté est le suivant:
public class Cat {
private int age;
public Cat(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
/**
* Sets the age for the current cat
* @param age an integer with the valid values between 0 and 25
* @return true if value has been assigned and false if the parameter is invalid
*/
public boolean setAge(int age) {
//Validate your parameters, valid age for a cat is between 0 and 25 years
if(age > 0 && age < 25) {
this.age = age;
return true;
}
return false;
}
}
Mais on pourrait penser que cette deuxième approche est beaucoup plus sûre:
public class Cat {
private int age;
public Cat(int age) {
//Use the modifier instead of assigning the value directly.
setAge(age);
}
public int getAge() {
return this.age;
}
/**
* Sets the age for the current cat
* @param age an integer with the valid values between 0 and 25
* @return true if value has been assigned and false if the parameter is invalid
*/
public boolean setAge(int age) {
//Validate your parameters, valid age for a cat is between 0 and 25 years
if(age > 0 && age < 25) {
this.age = age;
return true;
}
return false;
}
}
Voyez-vous un schéma similaire dans votre expérience ou est-ce juste que je suis malchanceux? Et si vous le faites, alors qu'est-ce qui, selon vous, est à l'origine de cela? Y a-t-il un inconvénient évident à utiliser des modificateurs des constructeurs ou sont-ils simplement considérés comme plus sûrs? Est-ce autre chose?
la source
Réponses:
Raisonnement philosophique très général
Typiquement, nous demandons à un constructeur de fournir (comme post-conditions) quelques garanties sur l'état de l'objet construit.
En règle générale, nous nous attendons également à ce que les méthodes d'instance puissent supposer (comme conditions préalables) que ces garanties sont déjà valables lorsqu'elles sont appelées, et elles doivent seulement s'assurer de ne pas les rompre.
L'appel d'une méthode d'instance à l'intérieur du constructeur signifie que certaines ou toutes ces garanties peuvent ne pas encore avoir été établies, ce qui rend difficile de déterminer si les conditions préalables de la méthode d'instance sont remplies. Même si vous le faites bien, il peut être très fragile face, par exemple. réorganiser les appels de méthode d'instance ou d'autres opérations.
Les langages varient également dans la façon dont ils résolvent les appels aux méthodes d'instance héritées des classes de base / remplacées par les sous-classes, alors que le constructeur est toujours en cours d'exécution. Cela ajoute une autre couche de complexité.
Exemples spécifiques
Votre propre exemple de la façon dont vous pensez que cela devrait ressembler est lui-même faux:
cela ne vérifie pas la valeur de retour de
setAge
. Apparemment, appeler le passeur n'est pas une garantie d'exactitude après tout.Erreurs très faciles comme en fonction de l'ordre d'initialisation, telles que:
où mon enregistrement temporaire a tout cassé. Oups!
Il existe également des langages comme C ++ où l'appel d'un setter à partir du constructeur signifie une initialisation par défaut gaspillée (ce qui vaut au moins pour certaines variables membres)
Une proposition simple
Il est vrai que la plupart du code n'est pas écrit comme ça, mais si vous voulez garder votre constructeur propre et prévisible, tout en réutilisant votre logique de pré et post-condition, la meilleure solution est:
ou encore mieux, si possible: encodez la contrainte dans le type de propriété, et faites-lui valider sa propre valeur lors de l'affectation:
Et enfin, pour une complétude complète, un wrapper auto-validant en C ++. Notez que bien qu'il fasse toujours la validation fastidieuse, car cette classe ne fait rien d'autre , il est relativement facile de vérifier
OK, ce n'est pas vraiment complet, j'ai laissé de côté divers constructeurs et affectations de copie et de déplacement.
la source
Lorsqu'un objet est en cours de construction, il n'est par définition pas entièrement formé. Considérez comment le setter que vous avez fourni:
Et si une partie de la validation
setAge()
comprenait une vérification par rapport à laage
propriété pour s'assurer que l'objet ne pouvait que vieillir? Cette condition pourrait ressembler à:Cela semble être un changement assez innocent, car les chats ne peuvent se déplacer dans le temps que dans une seule direction. Le seul problème est que vous ne pouvez pas l'utiliser pour définir la valeur initiale de
this.age
car il suppose quethis.age
déjà une valeur valide.Éviter les setters dans les constructeurs montre clairement que la seule chose que fait le constructeur est de définir la variable d'instance et d'ignorer tout autre comportement qui se produit dans le setter.
la source
Quelques bonnes réponses ici déjà, mais jusqu'à présent, personne ne semblait avoir remarqué que vous posiez ici deux choses en une, ce qui crée une certaine confusion. À mon humble avis, votre message est mieux vu comme deux questions distinctes :
s'il y a une validation d'une contrainte dans un setter, ne devrait-elle pas aussi être dans le constructeur de la classe pour s'assurer qu'elle ne peut pas être contournée?
pourquoi ne pas appeler directement le setter à cet effet depuis le constructeur?
La réponse aux premières questions est clairement "oui". Un constructeur, quand il ne lève pas d'exception, devrait laisser l'objet dans un état valide, et si certaines valeurs sont interdites pour certains attributs, alors cela n'a absolument aucun sens de laisser le ctor contourner cela.
Cependant, la réponse à la deuxième question est généralement «non», tant que vous ne prenez pas de mesures pour éviter de remplacer le setter dans une classe dérivée. Par conséquent, la meilleure alternative est d'implémenter la validation des contraintes dans une méthode privée qui peut être réutilisée à partir du setter ainsi que du constructeur. Je ne vais pas ici pour répéter l'exemple @Useless, qui montre exactement cette conception.
la source
Voici un peu stupide de code Java qui illustre les types de problèmes que vous pouvez rencontrer avec l'utilisation de méthodes non finales dans votre constructeur:
Lorsque j'exécute cela, j'obtiens un NPE dans le constructeur NextProblem. C'est un exemple trivial bien sûr, mais les choses peuvent se compliquer rapidement si vous avez plusieurs niveaux d'héritage.
Je pense que la principale raison pour laquelle cela n'est pas devenu courant est qu'il rend le code un peu plus difficile à comprendre. Personnellement, je n'ai presque jamais de méthodes de définition et mes variables membres sont presque toujours (jeu de mots) finales. Pour cette raison, les valeurs doivent être définies dans le constructeur. Si vous utilisez des objets immuables (et il existe de nombreuses bonnes raisons de le faire), la question est théorique.
Dans tous les cas, c'est un bon objectif de réutiliser la validation ou une autre logique et vous pouvez la mettre dans une méthode statique et l'invoquer à la fois du constructeur et du setter.
la source
name
champ).this
référence à une autre méthode, car vous ne pouvez pas être sûr qu'elle soit complètement initialisée.Ça dépend!
Si vous effectuez une validation simple ou émettez des effets secondaires coquins dans le setter qui doivent être frappés à chaque fois que la propriété est définie: utilisez le setter. L'alternative consiste généralement à extraire la logique du setter du setter et à invoquer la nouvelle méthode suivie de l'ensemble réel à la fois du setter et du constructeur - ce qui est en fait la même chose que d'utiliser le setter! (Sauf que maintenant vous maintenez votre setter, certes petit, à deux lignes à deux endroits.)
Cependant , si vous devez effectuer des opérations complexes pour organiser l'objet avant qu'un setter particulier (ou deux!) Puisse s'exécuter avec succès, n'utilisez pas le setter.
Et dans les deux cas, ayez des cas de test . Quelle que soit la voie que vous empruntez, si vous avez des tests unitaires, vous aurez de bonnes chances d'exposer les pièges associés à l'une ou l'autre option.
J'ajouterai également, si ma mémoire de cette époque est même à distance exacte, c'est le genre de raisonnement qu'on m'a aussi enseigné à l'université. Et ce n'est pas du tout en décalage avec les projets réussis sur lesquels j'ai eu la chance de travailler.
Si je devais deviner, j'ai raté un mémo "code propre" à un moment donné qui a identifié certains bogues typiques pouvant survenir en utilisant des setters dans un constructeur. Mais, pour ma part, je n'ai pas été victime de tels bugs ...
Donc, je dirais que cette décision particulière n'a pas besoin d'être radicale et dogmatique.
la source