14 sept. 2006

La guerre des accesseurs

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 ?

  1. un setter simple qui initialise une variable membre avec une valeur passée en paramètre
  2. un setter qui vérifie la valeur du paramètre et qui le contraint à une valeur particulière avant de l'affecter
  3. 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, septembre 14 2006, 21:05 par Victor Nicollet

Je partage ce point de vue, mais pas pour les mêmes raisons. D'abord, je conçois parfaitement qu'on puisse utiliser un setter pour vérifier les données entrées --- en utilisant des exceptions pour signaler les opérations invalides, comme pour n'importe quelle opération. Ensuite, il est envisageable d'effectuer des opérations de maintenance interne sur l'objet, qui ne sont visibles de l'extérieur que comme le changement de la propriété modifiée --- par exemple, setMask() sur un objet qui est représenté en interne comme un arbre, et qui donc met à jour tout le contenu des nodes de manière à correspondre au nouveau masque.

En revanche, je m'oppose aux setters en général parce qu'il s'agit de voodoo-OO: agréger des objets en une classe et coller des getters/setters par-dessus n'apporte ni abstraction, ni encapsulation dignes de ce nom. Dans la plupart des cas, il est préférable de modifier les responsabilités de la classe (à travers un glissement sémantique de son nom vers quelque chose de plus compréhensible par l'humain) et d'obtenir des méthodes au comportement plus abstrait que setXXX. Ainsi, resize() est un meilleur nom (et une meilleure fonctionnalité) que setSize().

Bien sûr, il reste des cas où un setter est la meilleure chose à faire (par exemple, setParent dans un noeud d'arbre, qui lance une exception si un cycle est ainsi créé). Mais c'est rarement le cas.

2. Le vendredi, septembre 15 2006, 00:49 par Christophe Moustier

J'ai plus le sentiment que ce qui cloche ce n'est pas le concept de setter qui est en jeu, mais la notion de "signifié" associé à un mot (cf Lacan).
Ce questionnement interroge en fait la sémantique et des notions métaphysiques telles que l'ontologie (cf Heideger - "Qu'est-ce qu'une chose").

Personnellement, tant que le "contrat" de l'API spécifie le comportement d'altération de la propriété est présent, cela constitue un bon palliatif au problème signifiant/signifié que nos pauvres cervelles d'informaticiens ne connaissent que très rarement.

3. Le vendredi, septembre 15 2006, 08:13 par Christophe Moustier

Pour revenir brièvement sur le sujet, je dirai aussi qu'historiquement, les accesseurs sont issus du langage java (du moins, à ma connaissance !) qui utilisait la convention de nommage getData et setData où Data était le nom d'un attribut d'une classe. Cette convention de nommage était alors utilisée par le BDK (Beans Developper's Kit) pour afficher la propriété dans l'environnement graphique de construction des IHM.

Maintenant, pour te rassurer, j'ai entendu parlé "d'accesseurs faibles et forts" où "comment pratiquer la sodomie des drosophiles" ;)

4. Le vendredi, septembre 15 2006, 11:15 par Emmanuel Deloget

Ah ! J'ai enlevé tout un paragraphe sur les accesseurs forts et faibles, de peur de retrouver ce site référencé par thedailywtf.com !

C'est effectivement la dualité signifiant/signifié (ce que j'englobe, peut-être incorrectement, dans la notion de sémantique de la fonction) qui pose selon moi un problème. Lorsque le signifiant diffère du signifié, un effort supplémentaire est requis pour ajuster le signifiant à sa nouvelle définition - et franchement, je crois que je fais déja assez d'efforts supplémentaires. Rappelle moi toutefois de lire Heideger et Lacan (je dois pouvoir trouver ça quelque part).

Au final, le problème peut effectivement être vaguement réglé par une documentation adéquate - mais soyons honnètes : si on calcule le ratio "projets documentés à ce niveau de détail" / "nombre total de projets", j'ai peur qu'on atteigne une valeur plus proche de 0 que de 1 :)

Victor, pour revenir sur ce que tu dis sur les setters et les exceptions, j'avoue que l'idée est séduisante - et pas incompatible avec ce que je dis. En effet, l'opération consitant à capturer les entrées non valides pour générer une exception ne modifiant ni le paramètre ni la valeur enregistrée, elle reste, selon moi, compatible avec la sémantique d'un setter (étrangement, dans ce cas, le fait de retourner un code d'erreur n'est pas équivalent à générer une exception, du fait de la modification de la signature de la fonction). Quand à ton exemple de setMask(), il reste lui aussi dans le cadre de mon explication - enfin, je le trouve quand même un peu "border-line", et renommer la fonction en makeMask() ou setupMask() offrirait peut-être une vision moins sémantiquement fermée de l'opération sous-jacente. Il n'en reste pas moins que l'opération effectuée est de changer le masque en utilisant la valeur précisée (on retrouve la définition de "to set"). Que la représentation du masque soit complexe ou simple, ce n'est qu'un détail d'implémentation. Au final, on arrive même à un niveau que je pense correct d'encapsulation.

Sinon, il est évident l'abus de setter et de getter (qui, malheureusement, est monnaie courante) abouti à une désencapsulation puisqu'elle expose les détails d'implémentation de la classe aux utilisateurs de celle ci - et force donc ces derniers à connaître son fonctionnement interne afin de faire la meilleure utilisation de l'interface proposée. Je ne suis même pas sur que ce qu'on obtienne alors soit de la POO :)

Il y a quelque chose que je n'ai pas dit: le verbe set peut signifier d'autres choses, tout comme on peut utiliser le sens qui est le sien pour effectuer une action particulière. Les setters ne sont pas les seules fonctions qui peuvent l'utiliser pour composer leur nom.

A bientôt,

-- Emmanuel D.

PS: il va falloir que quelqu'un se mette à recenser les différents concepts liés à la programmation orientée programmation (POP). L'idée de Voodoo-OO me plait bien ;)

5. Le vendredi, septembre 15 2006, 21:51 par Victor Nicollet

Je pense que tu voulais écrire "The daily WTF" (thedailywtf.com)

6. Le dimanche, septembre 17 2006, 11:16 par Emmanuel Deloget

Je m'ai trompé ! (et je m'ai corrigé) :)

7. Le dimanche, septembre 17 2006, 12:54 par Victor Nicollet

Sauf que non, l'url est toujours incorrecte :)

Au passage, la définition d'AskOxford qui correspond le mieux est, à mon humble avis, la 5, et non la 2: decide on or fix (a time, value, or limit).

8. Le lundi, septembre 18 2006, 14:59 par Emmanuel Deloget

Pas facile de voir laquelle est la meilleure parmi les définitions proposées. Effectivement, celle là convient tout autant. Je médite...

Pas sur de toute façon que celà change l'idée générale de spécifier un état précis ("to decide on" est assez limitatif: le "on" renforce l'action - ainsi, l'acte de décision n'est pas anoodin, et le fait d'autoriser le programme à modifier cette décision à l'insu de celui qui l'a prise entre quand même en contradiction avec la définition).

Et l'URL est censée être corrigée, maintenant ;)

9. Le mardi, mars 6 2007, 15:07 par fabrizio


Hors, quels sont les trois types de setters que nous rencontrons ?

pas seulement: un setter peu délegué un traitement, cas typique dans les gui, par exemple le design pattern Observer, peu difficilement ce passé d'un setter.

void x :: setObserver( Observer* x ) { _observer = x; }

void x :: setValue( int value)
{
if( _value != value )
{
_value = value;

if ( _observer )
_observer -> notifyValueChanged();

}
}

10. Le mercredi, mars 7 2007, 11:05 par Emmanuel Deloget

Merci pour ton commentaire - je n'y avais pas pensé.

Ceci dit, pour ma part, j'aurais nommé cette fonction x::changeValue() (qui, d'ailleurs, est cohérent avec l'appel à notifyValueChanged()). Il se peut que d'autres noms soit mieux adapté.

Bon, soyons honnêtes. J'ai regardé dans ma base de code source, et j'ai trouvé plein de noms différents, selon que le sujet gère un valeur ou une collection de valeurs. J'ai même, sur un code vieux de plusieurs siècles (disons, quelques années), retrouvé une tripotée de set_xxx() qui faisait la même opération. Je me suis alors rendu compte que c'était vraiment mesquin: nos opinions changent au fil des années (ça doit être du à ce que les anciens appelaient "l'expérience"), et mon opinion actuelle n'a que valeur d'information - rien ne dit que je suis dans le vrai :)

Aujourd'hui, j'aurais tendance à écrire ce même code sous la forme d'une méthode change_value(), qui appellerait une fonction propagate_change(), qui bouclerait sur les observeurs pour appeler leurs méthodes notify(). Mais j'éviterais quand même de nomme la fonction top level "set_value()" - pour la raison que cette fonction est plus complexe que seul son nom ne le laisse supposer.

11. Le mardi, septembre 4 2007, 13:47 par screetch

dans la definition de set il y a la connotation de chemin a accomplir, et je ne suis pas du tout d'accord avec ta definition francaise de set : "initialiser a une valeur". Elle est fausse, car set signifie amener dans l'etat final sans se preoccupper de l'etat courant; voila le resultat souhaité. Je pense que ta traduction francaise est fausse.

Et quand bien meme elle serait vraie, le fait de la nommer "set" de maniere simple, n'est ce pas ce qu'on appelle l'encapsulation ? ne pas publier le fonctionnement interne mais seulement le fonctionnement externe ? en ce sens, je refuse d'appeler mes methodes "changeValue_NotifyListeners". c'est setValue, point barre. le comportement interne, le fait que des gens "ecoutent" l'etat ou pas, n'est pas du ressort de celui qui demande un changement d'etat de l'objet. La simplicité de "set..." est alors bien ce que l'on cherche.

12. Le mercredi, septembre 5 2007, 14:04 par Emmanuel Deloget

J'ai lu récemment ce que tu as écrit sur developpez.com - et tu marques des points (bon, je n'ai pas saisi l'allusion à la notion de sémantique et de moquerie supposée de tes collègues). Je me rassure en me disant que j'ai écrit cet article il y a plusieurs mois - presque un an, une éternité pour changer d'opinion.

Sauf que je n'ai guère changé d'opinion. Je considère toujours les setters comme étant des points faibles dans une classe et je n'aime toujours pas la sémantique du mot set en tant qu'action. J'aimerais bien pouvoir te dire que j'ai raison (j'adorerais avoir toujours raison), mais au vu du nombre de commentaires liés à cet article, il faut quand même que je me pose des questions.

Ceci dit, je reconnais que j'y suis allé un peu fort, notamment en ce qui concerne l'encapsulation. Effectivement, retrouver des informations concernant des détails d'implémentation dans le nom d'une fonction est une erreur, et je ne voudrais pas en arriver là. Ce n'est pas ce que je propose (en filigrane, il est vrai; Mat007 a vu juste: c'est surtout le terme que je critique, pas l'idée). Je propose surtout d'utiliser d'autres verbes, moins couramment utilisée, moins omniprésents que set et plus précis que ce dernier - qui est plus que général. Dans ton premier billet sur developpez.com tu dis:

La bounding box d'un objet 3d est un détail d'implémentation; a chaque fois que j'ajoute un point, je ne suis pas sensé appeler la méthode "setValueAndUpdateBoundingBox", c'est ca l'encapsulation aussi........
Personnellement, un mesh 3D auquel je peux ajouter des points et modifier les points existants utiliserai une méthode "addPoint(const point3d&)" et une méthode "movePoint(int index, const point3d&)" plutôt que "set_quelquechose()". Dans l'exemple que j'ai utilisé dans mon article, plutot que "setExtent()" j'aurais utilisé "inflate()" ou son pendant (pour réduire la taille). Pour un observer, j'ai tendance à utiliser "changeSomething()" plutôt que "setSomething()".

Bref: chaque fois que je peux éviter d'utiliser le mot set, je le fait. Au final, j'estime que j'y gagne en clarté et en sémantique. Je n'ai pas encore formaliser ma façon de voir les choses au niveau du design d'application, mais dans ma vision, la sémantique d'une architecture a une grande importance. Je me trompe peut-être - et il est tout à fait possible qu'une fois cette formalisation effectuée je m'aperçoive de mon erreur. Auquel cas je ferais un méa culpa publique (pas que j'aime ça, mais le déshonneur ne tue guère plus que les samouraïs, et je ne suis pas un samouraï).

Ceci dit, si je reprends l'exemple que tu as cité dans ton post, le nom setValue() me semble adéquat. Il y a peut être mieux, mais en tout cas, ça n'est pas suffisamment imprécis pour que je cherche une alternative. Ceci dit, la valeur que tu stockes (tiens, "storeValue()" ? non, c'est bon, j'arrête) n'est pas une propriété directe de l'instance - elle est stockée dans un conteneur. Est-ce que cela peut avoir une influence? (c'est une question ouverte).

Et pour terminer, j'aimerais revenir sur cette histoire de définition. Pour ce qui est de la définition du Oxford Dictionnary, trois points seulement sont d'intérêt dans notre cas

  • 1 put, lay, or stand in a specified place or position. = mettre, poser ou rester à un endroit ou dans une position spécifique
  • 2 put, bring, or place into a specified state = mettre, ammener ou placer dans un état spécifique (c'est la définition que j'ai choisi)
  • 5 decide on or fix (a time, value, or limit). = (se) décider ou fixer (une heure, valeur ou limite)

La première (1) définition est utilisée pour parler d'un objet ou d'un corps: I'm setting in my flat, so you can come whenever you want. La seconde (2) et la troisième (5) définitions peuvent être utilisée pour décrire l'action d'un setter - elle sont dans ce contexte strictement identique (merci à Victor pour me l'avoir fait remarquer). Ma définition est fausse dans le sens ou initialiser représente une action qui a lieu pour la première fois. J'aurais du, effectivement, utiliser un autre verbe (et donc transformer notre définition en "amener une variable dans un état déterminé"). Et effectivement, la définition ne spécifie pas si on doit se soucier ou non de l'état courant - le seul point important est l'état final.

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet