L’amélioration de la structuration du code d’un programme, la séparation du métier et des concepts manipulés d’avec les aspects techniques, est le Saint Graal que poursuivent sans fin les développeurs. Dans ce but, de nombreuses techniques ont fait leur apparition dont nous pouvons citer les traits ou les annotations. A côté de ceux-ci, il existe une technique élégante et uniforme que sont les slots. Mais, que sont ces derniers et en quoi peuvent ils nous aider dans notre quête ?
Un adage veut qu’un bon développeur soit un développeur paresseux. Ce qui se cache derrière cette paresse est l’aptitude du codeur à automatiser ses tâches redondantes et souvent ennuyeuses et de tout faire pour faciliter son boulot. L’avantage de cette attitude est évidemment pour l’entreprise un gain de productivité à terme. En fait, à y regarder de près, le développeur ne fait que s’aligner aux lois de la physique : minimiser l’entropie. Un des aspects de cette caractéristique est, pour le programmeur, de minimiser la duplication de codes et de factoriser les responsabilités et, pour ce faire, il va user de techniques et de moyens qu’il a en sa possession via l’outillage et surtout son langage de programmation.
La structuration du code est l’une de ces techniques. Elle est vieille de plus de 30 ans mais n’a cessé et ne cesse de continuer à évoluer. L’AOP (Aspect-Oriented Programming), les traits, les mixins, les closures, etc. ne sont que des aspects de celle-ci dans leur objectif de modulariser non seulement le code métier mais aussi et surtout le code transversal au métier et aux fonctionnalités du logiciel. Avec l’AOP, nous pouvons définir des composants d’ordre technique comme la génération de traces ou la validation des autorisations d’appel d’opérations. Ces composants sont ensuite tissés aux objets métiers à l’exécution ou à la compilation ; l’AOP permet de maintenir d’un côté les caractéristiques techniques et de l’autre les modules métiers, évitant ainsi de les entacher de considération techniques. Les mixins et les traits sont deux concepts différents permettant de définir des propriétés, d’ordre technique ou métier, transversales aux objets métiers puis de les associer à ces derniers de manière statique ou dynamique. Par exemple, l’ordonnancement ou encore la persistance peut être définie dans un mixin ou un trait, indépendamment des objets métiers des applications. Ces deux techniques ont été popularisées respectivement par les langages Ruby et Self. Les annotations, popularisées par Java, constituent une autre technique qui permet de qualifier sémantiquement des objets et d’attacher à celles-ci un comportement qui peut aller jusqu’à la génération de codes ; par exemple, des objets peuvent être annotés avec des marqueurs de gestion de transactions ou encore d’annotations métiers propres au domaine de l’application.
Pourtant, il existe un autre concept popularisé par Self en même temps que les traits : les slots. Ces derniers ne sont pas nouveaux puisqu’ils furent introduit par CLOS (Common Lisp Object System). (Dans CLOS comme dans Self, ils sont appelés descripteurs de slot). On retrouve ces derniers dans le langage Io et actuellement un travail est réalisé pour les intégrer à terme dans Pharo. Mais qu’est-ce qu’un slot ou descripteur de slot ? Dans le contexte d’un langage orienté-objet, un slot est la réification du point d’accès aux propriétés d’un objet ou, plus exactement, du mécanisme d’envoi/réception de messages (cf. mon billet sur les différentes POO). Tout accès aux propriétés d’un objet se fait par l’intermédiaire des slots qui, non seulement peuvent contrôler l’accès en lecture et en écriture de ces propriétés, mais aussi contenir des informations sémantiques sur celles-ci. Ils constituent une interface homogène d’accès aux propriétés de l’objet. Dans Self et Newspeak, le slot est un objet particulier qui agit comme une méthode retournant toujours une valeur (que celle-ci soit calculée dans le cas d’une méthode ou mémorisée dans le cas d’un attribut). Selon les langages de programmation, les slots peuvent n’être définis que pour l’accès aux champs d’un objet. Parce que c’est aussi un objet, le slot peut être manipulé comme n’importe quel autre objet du langage et, par voie de conséquence, il est possible alors de lui affecter aussi un comportement.
Pour illustrer mon propos, voici un petit exemple dans un pseudo-langage dans lequel le symbole .
représente un slot (ou plus exactement son accès) et self
fait référence à l’objet lui-même (ici le Point
) :
Point.x = 3
Point.y = 8
Point.move = (abs, ord) {
self x += abs
self y += ord
self
}
Point.move.afterInvocation = (abs, ord) {
View current update(self)
}
Ici, nous ajoutons dynamiquement trois slots aux points. Dans les deux premières lignes, les slot réfèrent respectivement les coordonnées x
et y
des points initialisées ici avec des valeurs. Quant à la troisième ligne, le slot réfère une opération move
. Les deux premiers slots retourneront respectivement 3
et 8
tandis que le troisème, une fois son action accomplie, retournera par défaut le point lui même. La dernière ligne spécifie quoi faire à la suite de l’invocation de l’opération move
: mettre à jour l’IHM dans laquelle est dessiné le point. Dans mon pseudo-langage, par homogénéité, les propriétés d’un slot (par exemple afterInvocation
) sont elles aussi des slots et ici le slot afterInvocation
est spécifique à tous les slots et spécifie ce qui doit être fait une fois l’opération référée par le slot ait été invoquée.
Ainsi, au travers de l’exemple, nous pouvons constater que les slots de l’objet sont aussi des objets à qui, non seulement nous pouvons invoquer des opérations, mais aussi définir des slots. Parce qu’un slot peut représenter un point d’accès vers un autre objet, les relations inter-objets deviennent mécaniquement des citoyens de première classe au même titre que l’objet (ou la classe dans un langage orienté-classes). Pour illustrer ceci, imaginons un employeur qui a plusieurs employés et des employés qui sont rattachés à un et un seul employeur :
Employer.employees = List empty
Employer.employees.add.afterInvocation = (e) {
(e employer == self) ifFalse {
e employer = self
}
self
}
Employee.employer = None
Employee.employer.onWrite = (e) {
(e employees contains(self)) ifFalse {
e employees add(self)
}
self
}
Dans le code ci-dessus, nous définissons des slots respectivement d’ajout d’un employé à un employeur et de spécification d’un nouvel employeur à un employé. Chaque nouveau employé de l’employeur mettra automatiquement la référence employer
de l’employé avec son nouveau employeur. De même, chaque mise-à-jour de l’employeur de l’employé ajoutera ce dernier parmi les employés de son nouveau employeur :
anEmployee = Employee()
anEmployer = Employer()
anEmployer employees add(anEmployee)
Assertion assertThat(anEmployee employer, is(anEmployer))
Assertion assertThat(anEmployer employees, contains(anEmployee))
Les slots employees
et employer
représentent donc des relations qui peuvent être manipulés comme de simples objets et qui permettent de maintenir la cohérence des relations entre l’employeur et les employés. Nous pouvons aller plus loin en introduisant un mécanisme de création et de mise en relations hiérarchiques de slots comme avec les objets (ou les classes d’objets) :
ManyToOneSlot = Slot {
vars := [ $oppositeSlot ],
initialize = (slotSymbol, classSymbol) {
oppositeClass = System find(classSymbol)
this oppositeSlot = oppositeClass slot(slotSymbol)
this
},
onChange := (anInstance) {
((anInstance slot(this oppositeSlot)) == self) ifFalse {
(anInstance slot(this oppositeSlot)) write(self)
}
}
}
OneToManySlot = Slot {
vars := [ $oppositeSlot ],
initialize := (slotSymbol, classSymbol) {
oppositeClass = System find(classSymbol)
this oppositeSlot = oppositeClass slot(slotSymbol)
this
},
onChange := (anInstance) {
((anInstance slot(this oppositeSlot)) contains(self)) ifFalse {
(anInstance slot(this oppositeSlot)) add(self)
}
}
}
où $
désigne un symbole, this
fait référence au slot lui-meme, self
fait référence ici à l’objet qui porte le slot, slot
retourne soit le slot nommé par le symbole passé en argument, soit son instance associée à l’objet passé en argument, et onChange
indique quoi faire lors d’un changement d’état (ou de valeur) de l’objet référé par le slot. À l’utilisation :
Employer = Object {
slots := [ $employees ],
initialize := () {
self employees = ManyToOneSlot($employer, $Employee)
self
}
}
Employee = Object {
slots := [ $employer ],
initialize := () {
self employer = OneToMany($employees, $Employer)
}
}
De la même façon, il est alors possible de définir des slots dédiés à la persistance ou encore de combiner les slots entre eux pour réaliser des traitements transverses. Ceci et le fait d’ajouter du comportement aux slots ouvrent la porte à la méta-programmation sans commune mesure avec les autres concepts. Ce que permettent l’AOP, les annotations, certains usages des mixins et des traits, peuvent tous être réalisés par les slots et ceci d’une manière uniforme et cohérente avec les concepts de base du langage.
Ce petit billet vous a donné un petite présentation du concept de slot, illustrée avec un pseudo-langage. J’espère qu’il vous aura donné un aperçu de la puissance et de l’expressivité qui se cache derrière ce simple concept. Si vous souhaitez vous amuser avec les slots, je vous recommande d’essayer avec le langage Io qui est, selon moi, le plus simple à explorer.