La guerre des accesseurs
Par Emmanuel Deloget, jeudi 14 septembre 2006 à 10:00 :: Opinions :: permalien #12
Tags: C++, code dangereux, encapsulation
J'ai un dent contre les accesseurs. Enfin, pas contre tous, mais contre un accesseur particulier : le setter (nommé ainsi à cause de sa propension à être représenté par des noms du type setSomething()). Vous le verrez en lisant ce billet, la raison pour laquelle je ne l'aime pas peut vous paraître étrange, elle n'en reste pas moins forte selon moi.
Histoire d'être plus clair, effectuons ensemble un bref tour d'horizon de la question. En programmation orientée objet, on utilise les accesseurs pour accéder aux membres d'une classe. "On" appelle cela l'encapsulation des données (j'ai déjà expliqué qu'encapsuler les données n'était pas le but de l'encapsulation dans un billet antérieur, je ne vais donc pas recommencer mon laïus ici). Typiquement, on associe à chaque donnée deux fonctions membres, un getter et un setter, qui forment un couple d'accesseurs.
Dans l'ensemble, les getters peuvent se targuer d'avoir une (faible) raison d'être, puisque l'idée est de s'enquérir de l'état d'un objet ou d'une partie de son état est quand même relativement valide.
Là où le bat blesse, c'est sur les setters. Pour m'expliquer, une petite piqûre de rappel s'impose.
Un objet a un état, et il a la responsabilité de garder cet état cohérent dans le but de pouvoir le traiter[1]. Quelques fois, certaines variables de cet état sont décorellées des autres (c'est le cas dans certaines machines d'état où le choix d'un algorithme est décidé par la combinaison des états - OpenGL ou DirectX en sont de bons exemples). Mais dans la plupart des cas, la modification de l'état de l'objet ne peut se faire que de manière contrôlée.
Parfait me direz vous, il suffit de mettre du code de validation dans le setter et hop, encapsulé, ni vu ni connu. Bien évidemment, ceci se fait au mépris de la sémantique du nom de la méthode.
En effet, en anglais, set est un mot qui existe pour de vrai, et il a un sens précis. Si get peut être traduit par récupérer (ce qui en soit forme une abstraction de suffisamment haut niveau pour nous permettre d'implémenter le getter de la manière qui nous convient), set est beaucoup plus restreint - il signifie initialiser avec une valeur spécifiée
[2]. Mais quels sont les trois types de setters que nous rencontrons ?
- un setter simple qui initialise une variable membre avec une valeur passée en paramètre
- un setter qui vérifie la valeur du paramètre et qui le contraint à une valeur particulière avant de l'affecter
- un setter qui modifie une autre variable en fonction de la valeur du paramètre, dans le but de garder l'état global de l'objet cohérent
Si le premier est effectivement un setter, les deux autres (ou toute fonction linéaire de ces deux caractéristiques) n'en sont pas - ces méthodes représentent des actions modifiant l'état de l'objet, pas de simples initialisations. La différence est d'ordre sémantique, mais elle est de taille. Prenons un simple exemple :
void Shape::setExtent(int extent)
{
if (extent < -5) mExtent = -5;
else if (extent > 5) mExtent = 5;
else mExtent = extent;
}
// blah blah blah
shape.setExtent(10);
assert(shape.getExtent() == 10); // erreur...
Où donc, dans le nom setExtent() se cache l'information qui nous dit que le paramètre extent peut être modifié avant d'être affecté ? Un autre exemple :
void Shape::setExtent(int extent)
{
mExtent = extent;
mBox = computeBoundingBox();
}
Et là, qu'est ce qui nous dit que la boite englobante de l'instance de Shape sera modifiée si on modifie la taille de l'instance Shape ? Dans les deux cas, le nom setExtent() est très mal choisi - car la fonction réalisée va plus loin que ce que ce nom idiomatique suggère. Au final, il est plus apte à générer l'incompréhension qu'à décrire réellement ce qui se passe. Et c'est là mon principal problème, car en agissant ainsi je décide de ne pas suivre une des règles à la fois simple et fondamentale de le programmation : le nom d'un identfiant doit décrire au mieux sa fonction.
En fait, le problème vient du faible niveau d'abstraction donné par le nom de la méthode. On attends d'un setter nommé setXXX() qu'il effectue une opération très concrète (avec les problèmes que cela impose quand à la notion de séparation de l'interface et de l'implémentation) - ni plus, ni moins. Cependant, on l'utilise souvent pour faire bien plus que ce pour quoi il est prévu. Pour éviter ce problème il est nécessaire de garder à l'esprit l'abstraction que représente un objet particulier. Cette abstraction guide le nommage des méthodes faisant partie de l'interface de la classe. Si un nom de méthode devient trop concret, je réduis d'autant la portée de l'abstraction représentée par cette classe.
Ma conclusion : il faut éviter les méthodes nommées setXXX() : elle sont par trop imprécises quand à leur but réel. Et puis, dans la jungle des mots qui nous entourent, il y a certainement une locution qui conviendrait mieux.
Notes
[1] autrement dit: l'invariant (représenté par une condition sur un ensemble d'état) ne doit pas changer
[2] pour vérification, voyez la définition n°2 du Oxford Dictionary Online.
Commentaires
1. Le jeudi 14 septembre 2006 à 21:05, par Victor Nicollet
2. Le vendredi 15 septembre 2006 à 00:49, par Christophe Moustier
3. Le vendredi 15 septembre 2006 à 08:13, par Christophe Moustier
4. Le vendredi 15 septembre 2006 à 11:15, par Emmanuel Deloget
5. Le vendredi 15 septembre 2006 à 21:51, par Victor Nicollet
6. Le dimanche 17 septembre 2006 à 11:16, par Emmanuel Deloget
7. Le dimanche 17 septembre 2006 à 12:54, par Victor Nicollet
8. Le lundi 18 septembre 2006 à 14:59, par Emmanuel Deloget
9. Le mardi 6 mars 2007 à 15:07, par fabrizio
10. Le mercredi 7 mars 2007 à 11:05, par Emmanuel Deloget
11. Le mardi 4 septembre 2007 à 13:47, par screetch
12. Le mercredi 5 septembre 2007 à 14:04, par Emmanuel Deloget
:: Fil rss des commentaires de ce billet ::
Ajouter un commentaire