Bonjour. Dans cette séquence, on va voir quelque chose d'un
petit peu avancé en Pharo que sont les opérations
réflexives qui permettent de supporter le live
programming, le développement interactif qu'on utilise toujours.
On va se poser la question qu'est-ce qui se passe quand
je recompile une classe, quelles sont les opérations
réflexives mises en jeu pour implémenter tout ce qui est
nécessaire pour le développement interactif?
Je vais prendre un scénario.
Typiquement le scénario standard, c'est je définis une
classe, j'ajoute des méthodes sur cette classe, je crée
des instances de cette classe et ensuite, je vais changer
la définition de la classe et je vais rajouter des
nouvelles variables d'instances dans cette classe.
Donc ça veut dire qu'est-ce que je dois faire des
instances qui existent déjà, qui ont été créées, alors
qu'il y avait un attribut de moins dans la classe.
Ça veut dire qu'il va falloir être capable de migrer ces instances.
Il faut mettre en place un mécanisme pour transférer ces
instances sur la nouvelle version de la classe.
Et ensuite, je continue à travailler.
C'est ce qu'on va voir dans la suite.
On va voir les opérations qui permettent la recompilation
dynamique de classes, qui permettent la recompilation de
méthodes, qui vont permettre de migrer automatiquement et
de façon transparente pour le programmeur les instances
sur la nouvelle version de la classe.
Pour ça, il va falloir que je sois capable de récupérer
toutes les instances d'une classe, et ensuite que je sois
capable de changer tous ceux qui utilisaient ces
objets-là, de façon à leur changer leur pointeur pour
qu'ils utilisent les nouveaux objets.
Alors on commence par le début.
Comment est-ce que je fais pour récupérer toutes les
instances d'une classe? J'ai un message pour ça, si j'envoie
allInstances à une classe, ça me rend bien une collection
de tous les objets instances de cette classe.
C'est vraiment une collection de tous les objets, je peux les manipuler.
Ce sont des objets standards.
Par exemple, si je demande allInstances à la classe
Window, j'obtiens bien une collection d'objets Window.
Je peux récupérer le premier élément de la collection et
lui envoyer le message close puisque c'est une fenêtre.
Dans la classe window, il y a bien la méthode close qui est définie.
On a une autre primitive réflexif qui est pointersTo, qui
permet quand j'envoie ce message-là à un objet de
récupérer la collection de tous les objets qui détiennent
une référence sur cet objet.
Ça va être extrêmement pratique quand on va vouloir
changer des objets.
Justement on y vient.
Pour changer tous les pointeurs, c'est la primitive become.
L'idée, ça va être de dire je veux échanger 2 objets.
Pour échanger 2 objets et de façon symétrique, c'est-à-dire
que tous ceux qui pointaient sur cet objet-là dans le
système, et là on avait d'autres pointeurs sur cet
objet-là, je vais inverser. Je vais dire maintenant tous
ces éléments pointent sur celui-là et tous ceux qui
étaient ici, je casse ces liens, ils vont pointer
sur cet objet-là. C'est ça le become, c'est
inverser tous les pointeurs dans le système symétriquement.
Je vous donne un exemple ici.
Je crée un point 1 qui pointe vers l'objet.
00, une variable pt2 qui pointe vers l'objet 00 aussi
ici, une variable pt3 qui pointe vers le point 100
100. Et puis j'écris la primitive pt1 become pt3.
Ça veut dire que tous ceux qui pointaient sur l'objet
pointé par la variable pt1, maintenant ils vont pointer vers le point 3.
On voit que la variable pt2 pointait bien sur le même
objet que pt1 et maintenant elle pointe vers celui qui
était référencé par pt3. Pt3 lui, vu que c'est
symétrique, ça va pointer sur ce qui était pointé par
pt1, c'était celui-là, là-haut.
Et pt1 pointe bien vers celui qui était pointé par pt3.
On a bien les pointeurs qui ont été échangés de façon symétrique.
On a le pendant asymétrique qui est becomeForward qui
permet de dire que je veux changer les pointeurs.
Tous ceux qui pointaient vers cet objet-là vont
maintenant pointer sur cet objet-là, mais pas l'inverse.
C'est-à-dire ceux qui pointaient sur cet objet-là
continuent à pointer sur cet objet-là.
Je vous donne encore un exemple, le même exemple avec des
points où ici on effectue le becomeForward, et ce qu'on
peut voir c'est que ceux qui ont été impactés, ce sont
vraiment pt1 et pt2, pt3 n'a pas du tout été impacté par le becomeForward.
Tous ceux qui pointaient sur cet objet là n'ont pas été changés.
On a une autre primitive réflexive qui est adoptInstance,
qui permet de changer la classe d'un objet.
En gros je demande à une classe d'adopter une nouvelle
instance passée en paramètre. Changer la classe d'un
objet c'est extrêmement puissant mais ça a ses limites.
Il faut absolument que la classe de départ de l'objet,
donc la classe de anInstance, soit compatible du
point de vue du format avec la classe receveur ici.
Je ne peux pas changer n'importe quel objet, par exemple
en collection si ce n'en est pas une, et caetera, ça
dépend si c'est une collection indexée, et caetera.
Le format des objets va avoir une importance.
En fait une classe, qu'est-ce que c'est?
Si on revient à l'essence des classes.
Une classe, ce n'est ni plus ni moins qu'un format.
Le format va spécifier le nombre de variables d'instances,
le type de ces variables, nommer, indexer, vous vous
souvenez on l'a vu dans une séquence précédente.
Une classe a une superclass et un dictionnaire des méthodes.
On peut voir sur la classe Behavior, la classe Behavior c'est
une superclass de la classe class qui définit le
comportement de base des classes.
Je vous montre juste le minimum vital qu'on doit avoir
dans une classe, une classe a une superclass, un
dictionnaire de méthode et elle spécifie un ensemble de
format pour ses instances.
Dans cet exemple, je vais résumer un peu tout ce qu'on a
vu jusqu'à maintenant, tous les comportements réflexifs
que l'on a vus pour doter certaines
instances d'un comportement spécifique, qui leur est propre.
Je vous explique le code au fur et
à mesure. Ici, sur les trois premières lignes, on va
créer une instance de la classe behavior, je vous rappelle c'est une classe.
Je crée une instance de cette classe que j'appelle
behavior, donc là j'ai un objet behavior.
Je vais indiquer que cet objet behavior hérite de la
classe Model. Je fixe le
format de cet objet behavior.
Ensuite, je crée une instance de la classe Model.
J'ai un objet Model.
Et puis, la ligne importante de ce morceau de code est ici.
Je vais changer la classe de cet objet Model pour être la
classe de l'objet passé en paramètre, donc behavior.
Maintenant, je vais casser ce lien et je vais dire tu
deviens une instance de cette classe.
Cette classe est bien une sous-classe de la classe de Model.
Je vais être capable maintenant de compiler une nouvelle
méthode dans cette classe. Je vais compiler la méthode foo qui retourne 2.
Et c'est bien dans l'objet behavior que je fais ça.
On voit que la ligne importante, si j'envoie le message
foo à mon objet Model, il
va bien me répondre 2.
Pourquoi? Si vous vous souvenez de la méthode look-up,
j'envoie le message foo à l'objet Model, l'objet Model
commence la recherche de la méthode à exécuter dans sa
classe qui est l'objet behavior; et dans son dictionnaire
de méthode il y a bien la méthode foo.
Ça marche parfaitement.
Par contre, si je crée une nouvelle instance de la classe
Model, je lui envoie le
message foo. Donc là, j'ai une erreur qui est signalée,
message not understood puisque si j'applique l'algorithme
du look-up, je remonte dans la classe de cet objet, c'est
la classe Model et dans son dictionnaire de méthode, il n'y
a pas la méthode foo, ni dans aucune des superclass.
Ce mécanisme un peu avancé nous a permis de doter une
instance spécifique de la classe modèle d'un comportement particulier.
Je reprends le même exemple sur la classe Set pour que vous compreniez bien.
Je crée set1, c'est une instance de la classe Set.
La classe set a un dictionnaire de méthode avec la méthode add.
Je veux, pour une instance de la classe Set particulière,
changer cette méthode add qui a un comportement
particulier spécifique pour un set particulier.
Donc, je vais créer une classe anonyme, une classe
particulière, mon objet behavior d'avant qui hérite de la classe Set.
C'est de l'héritage.
Je vais avoir mon objet set2 qui est instance de cette
classe anonyme. Dans le dictionnaire de méthode de cette
classe anonyme, je vais redéfinir la méthode add en lui
mettant du comportement spécifique, le comportement qui va m'intéresser.
Et maintenant si j'envoie le message add à cet objet
set2, c'est bien cette méthode qui va être sélectionnée,
qui va avoir un comportement spécifique et différent de
la méthode add, si j'envoyais le message add à ce set-là.
On a bien 2 Set qui n'auront pas le même comportement
face au message add.
Je vous montre le code pour réaliser cette expérience.
Exactement comme avant, je vais créer une classe,
instance de la classe behavior. Je vais fixer la
super-classe à la classe Set. Je fixe le format.
Je vais compiler la méthode add dans cette classe anonyme.
La méthode add, je viens redéfinir la méthode add qui est
définie dans la classe Set.
Je fais un Transcript show juste pour faire un affichage,
pour dire que la méthode a été exécutée avec tel objet.
Je l'appelle super pour exécuter la méthode add qui se
trouve dans la classe Set.
Donc maintenant on va tester ce code.
Pour faire ça, je vais créer un premier Set.
Je vous rappelle j'ai la classe Set.
Je vais créer une première instance de cette classe que j'appelle Set.
Cette instance, je vais lui envoyer le message add1.
Donc là, c'est bien la méthode add de la classe Set qui va être utilisée.
Si je demande cette classe, je vais bien obtenir la classe Set.
Sauf que maintenant je vais exécuter cette primitive.
Donc, je vais demander à cet objet set de changer sa
classe et d'utiliser comme classe la classe Behavior que
j'avais avant, qui était une sous-classe de la classe
Set, donc Behavior.
On va le changer maintenant il va devenir une instance de
cette classe, qui lui, a une nouvelle version de la méthode add.
Ce lien a été cassé.
Si j'envoie maintenant le message add2
à cet objet, ça va sélectionner la méthode add de mon
objet Behavior, qui est une sous-classe de la classe set
et ça va bien exécuter ce code-là.
Je vais voir sur le transcript que la méthode a été
exécutée. Par contre, l'action va bien être exécutée in
fine puisque jusqu'ici super pour aller exécuter la
méthode add aussi de la classe Set.
Ça m'a permis de doter ou d'espionner les instances de
Set que j'aurais sélectionnées grâce à cette ligne-là.
En conclusion.
Tous les outils de réflexions qu'on a vu jusqu'à
maintenant sont extrêmement utiles, et permettent la
construction d'outils novateurs et innovants.
On peut vraiment expérimenter d'implémenter des nouvelles
choses grâce à ces primitives réflexives.
Ça promeut l'extensibilité du langage.
Par contre, il y a une règle d'or, quand on utilise la
réflexion il ne faut absolument pas l'utiliser à mauvais
escient dans vos programmes, dans du code métier.
Dans du code métier, on va limiter l'utilisation des
codes réflexifs puisque je vous ai dit qu'avec la
réflexion, on peut aller trop loin, violer l'encapsulation
des objets et vraiment s'affranchir des règles de la
bonne pratique de la programmation objet.
Donc c'est vraiment réservé à la construction d'outils.