Bonjour. Dans cette séquence, nous faisons suite à la
séquence précédente dans laquelle je disais que les
petites méthodes, c'était vraiment bien.
Ici, je vais vous montrer des exemples.
Pour revenir rapidement sur la séquence précédente, un
envoi de messages c'est un choix masqué parce qu'il peut
y avoir plusieurs implémentations possibles qui vont être appelées.
Pharo choisit laquelle en fonction du receveur du message.
C'est la hiérarchie de classes qui définit les choix:
plus on a de classes, plus on a de choix.
Et on peut ajouter de nouveaux choix facilement en créant
de nouvelles sous-classes.
Les sous-classes peuvent redéfinir et raffiner le code de leur super classe.
En fait, envoyer un message, c'est laisser la possibilité
à des sous-classes de changer le comportement.
On va s'intéresser au design pattern Template Method, que
vous trouverez dans le livre de "Design pattern".
Une template Method c'est quoi ?
C'est un squelette, ça définit globalement le
comportement d'un algorithme, par exemple avec des trous
à l'intérieur et ces trous vont pouvoir être redéfinis par les sous-classes.
Là, j'ai un exemple d'algorithme qui fait quelque chose
mais on ne sait pas trop quoi, qui ensuite va faire
quelque chose qui va être dans une méthode hookMethod1,
puis autre chose, puis quelque chose qui se trouve dans hookMethod2.
hookMethod1 et 2 puisque ce sont des méthodes à
part entière, elles vont pouvoir être redéfinies dans les sous-classes.
Il y a 2 possibilités pour hookMethod1 et 2.
Ces méthodes peuvent avoir
une implémentation par défaut ou pas.
Ici, on peut dire que hook1 a une implémentation par
défaut, c'est-à-dire que si la sous-classe ne propose
rien hook1 a une implémentation par défaut qui
va marcher. Ou alors, on peut dire que
hookMethod2 n'a pas d'implémentation et qu'il faut
absolument que la sous-classe définisse un comportement.
Là, vous avez le choix en tant que concepteur de la
classe, soit de fournir une implémentation, un
comportement par défaut, soit non.
On va prendre un premier exemple, l'exemple de printString.
L'idée, c'est que si j'envoie le message printString à un
objet, j'obtiens une chaîne de caractères qui représente cet objet.
Là, j'ai un délai. Je crée un délai
de 10 secondes. Si j'envoie le message printString à ce
delay là, je vois'a delay'et entre parenthèses, la
valeur en millisecondes du delay.
La méthode printString est implémentée dans la classe
objects de cette façon-là. En fait, elle envoie le
message printString LimitedTo. Cette implémentation-là
récupère une chaîne de caractères qui représente
l'objet, et ensuite si cette chaîne de
caractères-là est trop grande on peut la couper à une
certaine valeur, la limite, et concaténer à la fin point
point point etc. pour dire que la chaîne de caractères n'est pas terminée.
Là, l'important ici c'est de voir que
printStringLimitedTo envoie le message printOn à self.
C'est cette méthode-là qui va être redéfinie dans les sous-classes ou pas.
Si je regarde ce que retourne printString pour un
nœud et pour une pomme, là Node new retourne a Node.
Ça, c'est le printString de la classe Node.
Et là an Apple, c'est le prinString de la classe Apple.
On voit que ce comportement est le comportement par
défaut et il est implémenté dans la classe objects.
Le comportement par défaut du printString pour n'importe
quel objet, c'est 1,
récupérer le nombre de la classe, donc là Node et Apple,
et ensuite si ce nom de classe commence par une voyelle
alors on va préfixer par an, et si ça commence par une
consonne on va préfixer par a.
C'est ça qui nous permet d'avoir "a node" et "an apple.
" C'est l'implémentation par
défaut et il est possible de changer ce comportement par
défaut en ré implémentant printOn.
Donc là pour un delay on voit que le print string d'un
delay, ça commence par ce que retourne print on par
défaut, c'est-à-dire a délai, mais qu'ensuite on ajoute
entre parenthèses le delay en millisecondes.
C'est exactement ce que fait l'implémentation de la méthode printOn.
Elle commence par demander à la super classe le
printString par défaut, et ensuite ça ajoute la
parenthèse à un nombre de millisecondes préfixé
ainsi que la parenthèse fermante.
Là nous avons vu du raffinement.
La classe delay raffine l'implémentation de la méthode
print on de la classe objects.
Et une classe peut aussi redéfinir complètement le comportement.
C'est le cas pour les booléens par exemple.
Si j'affiche false, ça me retourne false et
donc on n'a pas a false, on a juste false.
Pour ça, on envoie juste la chaîne de caractères false
dans le stream passé en paramètre à printOn.
C'est une redéfinition complète.
Un autre exemple de redéfinition complète, c'est pour les intervalles.
Un intervalle définit un ensemble de valeurs entre une
valeur minimum et une valeur maximum.
L'intervalle 1 à 100 s'affiche parenthèse 1 to: 100 parenthèse.
L'intervalle 1 à 100 par "pas de 3", donc 1,
4, et caetera, affiche la même chose mais avec le pas en
plus. Comment est-ce que c'est implémenté?
La classe Interval redéfinit la méthode Print on et
envoie différents messages au aStream passé en paramètre.
Déjà on affiche une parenthèse, c'est la parenthèse qui s'affiche
ici et ici, puis on affiche la valeur initiale enfin le
début de l'intervalle, c'est-à-dire 1 et 1 qui se trouve ici.
Puis on affiche 2.
Puis on affiche la valeur finale.
C'est 100 et 100. Enfin, s'il y a un
pas différent de 1 qui est le défaut, on affiche
le pas et
à la fin on met la parenthèse.
Donc on a vu que printString utilisait le design pattern
template method pour que les classes puissent implémenter
leurs propres représentations textuelles.
On va avoir un autre exemple sous la forme du message
copie, qui permet de prendre un objet et
d'en créer un clone.
La copie d'objets c'est complexe et il peut y avoir
différentes stratégies, donc chaque classe peut décider à
quoi doit ressembler une copie d'une de ses instances.
Il existe une solution simple sous la forme d'un template
method qui met en jeu les méthodes copy et postCopy.
Copy, c'est le template method.
Postcopy, c'est le hook.
Dans la classe objects, il y a une méthode copy, je vous
laisse lire le commentaire, et cette méthode-là
envoie shallowCopy comme message à self, puis
postCopy sur le résultat.
ShallowCopy, ça crée un nouvel objet qui
partage toutes les variables d'instances avec l'objet de base.
On a 2 objets et toutes leurs variables d'instances sont les mêmes.
Donc si je modifie la variable d'instance d'un des
objets, je modifie la variable d'instance de l'autre objet.
Ça, c'est le comportement par défaut
de shallowCopy.
Maintenant suivant ce que va faire postCopy, ce partage
va se faire ou ne va pas se faire.
Si postCopy est vide, toutes les variables sont partagées
et les classes peuvent décider de mettre différentes
choses dans postCopy, pour partager certaines variables
et pas d'autres ou ne partager aucune variable.
Par défaut postCopy partage tout, donc ça retourne juste
l'objet en cours. Mais on peut tout à fait imaginer d'autres
implémentations de postCopy.
Là, par exemple dans la classe Bag, Bag c'est un type de
collection, et sa méthode de post Copy copie son contenu.
Un Bag a une variable d'instances contents et les
concepteurs de la classe Bag ont décidé que lorsqu'on
fait une copie d'un Bag, on ne veut pas partager la
variable contents, on veut 2 variables contents séparées.
Au début, elles auront la même valeur mais si j'en
modifie une, je ne modifierais pas l'autre.
Là, l'idée c'est que postCopy c'est un message qui est
envoyé à la copie, donc au nouvel objet, qui partage
toutes les variables d'instances, et si on ne veut pas
les partager il suffit d'en créer de nouvelles et de
décider quelle valeur on met dedans.
Là, dans ma variable contents, je vais mettre une copie
du contents d'origine. Comme ça je ne partage plus le
contents, chaque copie de mon Bag a son propre contents.
Pour le dictionnaire, on veut aller encore plus loin.
Un dictionnaire en fait c'est une collection de paires
clefs valeurs, donc une collection d'associations.
Et si on copie un
dictionnaire, on veut copier la collection.
Donc chaque dictionnaire va avoir sa propre collection de paires.
Mais on veut aussi que chaque paire soit différente de
façon à ce que si je modifie une paire d'un côté, ça ne
va pas modifier une paire de l'autre côté.
Pour ça, je copie le tableau mais je copie aussi chaque
paire à l'intérieur et c'est exactement ce qui se passe là.
En conclusion, le design pattern template method est
vraiment très très fréquent. En fait, c'est un signe de
bonne conception et si vous faites de la bonne conception
objet, vous allez vous retrouver avec des templates
methods partout, c'est parfaitement normal et c'est même une bonne chose.
N'hésitez pas à vous en servir, ça permet aux
sous-classes de définir du comportement et de modifier en
partie le comportement de la super classe.