Les accesseurs et modificateurs (alias setters et getters) sont utiles pour trois raisons principales:
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 lorsqu'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 que pensez-vous qui cause 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?
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é.
Votre propre exemple de la façon dont vous pensez que cela devrait ressembler est lui-même faux:
public Cat(int age) {
//Use the modifier instead of assigning the value directly.
setAge(age);
}
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:
class Cat {
private Logger m_log;
private int m_age;
public void setAge(int age) {
// FIXME temporary debug logging
m_log.write("=== DEBUG: setting age ===");
m_age = age;
}
public Cat(int age, Logger log) {
setAge(age);
m_log = log;
}
};
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)
Il est vrai que la plupart du code n'est pas écrit comme ceci, 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:
class Cat {
private int m_age;
private static void checkAge(int age) {
if (age > 25) throw CatTooOldError(age);
}
public void setAge(int age) {
checkAge(age);
m_age = age;
}
public Cat(int age) {
checkAge(age);
m_age = age;
}
};
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:
class Cat {
private Constrained<int, 25> m_age;
public void setAge(int age) {
m_age = age;
}
public Cat(int age) {
m_age = age;
}
};
Et enfin, pour une complétude complète, un wrapper auto-validant en C++. Notez que bien qu'il fasse toujours la validation fastidieuse, parce que cette classe fait rien d'autre, il est relativement facile de vérifier
template <typename T, T MAX, T MIN=T{}>
class Constrained {
T val_;
static T validate(T v) {
if (MIN <= v && v <= MAX) return v;
throw std::runtime_error("oops");
}
public:
Constrained() : val_(MIN) {}
explicit Constrained(T v) : val_(validate(v)) {}
Constrained& operator= (T v) { val_ = validate(v); return *this; }
operator T() { return val_; }
};
OK, ce n'est pas vraiment complet, j'ai laissé de côté divers constructeurs et affectations de copie et de déplacement.
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:
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;
}
Que se passe-t-il si une partie de la validation dans setAge()
inclut une vérification par rapport à la propriété age
pour s'assurer que l'objet ne peut que vieillir? Cette condition pourrait ressembler à:
if (age > this.age && age < 25) {
//...
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 cela suppose que this.age
A déjà une valeur valide.
Éviter les setters dans les constructeurs montre clairement que la chose seulement que le constructeur fait est de définir la variable d'instance et d'ignorer tout autre comportement qui se produit dans le setter.
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 séparé questions:
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 répéter l'exemple @Useless, qui montre exactement cette conception.
Voici un peu idiot de Java qui montre les types de problèmes que vous pouvez rencontrer avec l'utilisation de méthodes non finales dans votre constructeur:
import Java.util.regex.Pattern;
public class Problem
{
public static final void main(String... args)
{
Problem problem = new Problem("John Smith");
Problem next = new NextProblem("John Smith", " ");
System.out.println(problem.getName());
System.out.println(next.getName());
}
private String name;
Problem(String name)
{
setName(name);
}
void setName(String name)
{
this.name = name;
}
String getName()
{
return name;
}
}
class NextProblem extends Problem
{
private String firstName;
private String lastName;
private final String delimiter;
NextProblem(String name, String delimiter)
{
super(name);
this.delimiter = delimiter;
}
void setName(String name)
{
String[] parts = name.split(Pattern.quote(delimiter));
this.firstName = parts[0];
this.lastName = parts[1];
}
String getName()
{
return firstName + " " + lastName;
}
}
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.
Cela 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 du setter et du constructeur - qui est effectivement 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 un setter particulier (ou deux!) pourrait s'exécuter avec succès, n'utilisez pas le setter.
Et dans les deux cas, ont 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.