Donc cette vidéo est un Redo pour le MOOC.
Re-do dans le sens où on vous demande de le refaire, de
suivre la vidéo et pas à pas de le refaire.
C'est aussi une invitation à regarder comment on code.
Donc moi je ne vais pas suivre un script très précis, je
vais essayer de le faire mais si j'ai des bugs ou des
choses comme ça, je vais les traiter au fur et à mesure.
Donc l'idée de cette vidéo, c'est de faire un petit
langage qu'on retrouve dans les jeux de rôles comme
"Donjons et Dragons", qui nous disent par exemple, ceux
qui ont ce genre de petites expressions, qu'est-ce que
veut dire cette expression ?
Elle veut dire: tu dois lancer 2 dés à 20 faces et 1 dé à 4 faces.
Et vous allez voir que ça peut être une expression Pharo.
Et donc, dans cette vidéo, on va
implémenter une classe qui représente un dé et une classe
qui représente une poignée de dés.
Donc, on va commencer.
Donc pour cela, on va commencer par définir un package.
Donc ce package, on va l'appeler "Dice".
Donc je n'ai pas trop envie de voir
d'autres choses. Alors cette
vidéo je ne vais pas la faire en mode codée dans le
débugger, vous l'avez vu dans une autre vidéo, je vais le
faire au cas par cas, de manière opportuniste.
Donc là, je vais définir la classe "Die", qui a un
certain nombre de faces.
Je la compile. Je vais rajouter un commentaire de classe.
Alors pour le moment, pas grand-chose parce que c'est très très simple.
Ce qu'on va commencer à faire, on va faire en sorte qu'on
puisse initialiser notre objet.
Donc, je vais le faire comme ça.
J'appelle une initialisation de la super classe, et je
vais mettre par défaut 6 faces à mon dé, parce que ce
sont les dés les plus courants.
Maintenant je vais commencer aussi à développer une
classe de tests, histoire d'être sûr que ce qu'on fait ne
casse pas ce qui a déjà été fait.
Donc les classes de tests, elles sont sous-classes de la classe TestCase.
Et on va l'appeler "DieTest".
Voilà, j'ai ma classe de tests.
Donc un des premiers tests qu'on va faire, alors il ne
faut pas les faire tout le temps comme ça, mais en tout
cas, celui-là, j'aimerais bien commencer par avoir un test qui fonctionne.
Comme là, on n'a pas grand-chose, je vais dire que l'initialisation est ok.
Donc c'est aussi une façon pour moi de vous montrer
comment on peut tester qu'on peut capturer des exceptions
ou que des exceptions ne doivent pas arriver.
Donc là, ce que je suis en train de dire, je veux dire:
Die new should not raise error.
Donc qu'est-ce que ça veut dire ?
Ça veut dire, quand j'exécute ce morceau de code "Die
new", il n'y a pas d'erreurs qui doivent arriver.
Donc je vais classifier mon test et je vais le faire tourner.
C'est vert. Très bien.
Donc maintenant ce que j'aimerais faire, c'est définir la
méthode qui fait tourner un dé, qui fait un lancé de dé.
Donc, je sais que dans Pharo, il doit y avoir une méthode
qui s'appelle "at Random".
At Random, qu'est-ce qu'elle fait ?
Elle me permet d'avoir… Ok.
Donc maintenant je vais regarder l'implémentation pour m'assurer
que c'est bon. AtRandom, voilà, qu'est-ce qu'il fait?
Rend un entier au hasard entre 1 à self, donc c'est parfait.
Donc ce que je vais faire, je vais définir une nouvelle
méthode dans Operations.
Qu'est-ce qu'elle va faire ?
Roll. Je vais lui dire: tu me rends "faces
atRandom".
Donc on va écrire un test pour ça.
TestRolling. Donc qu'est-ce qu'on fait là?
On va créer un dé. d : = die
new.
Et maintenant, je vais faire "1000 timesRepeat".
Quoi ? "D Roll".
Donc ça, je veux que ce soit entre 1 et 6.
"Between: and:", c'est
bien. "Between 1 et 6".
Alors, ce n'est pas très bien parce que là, on a fait un
test que pour les dés à 6 faces, on aurait pu dire que ça
fonctionne en fonction du nombre de faces de notre dé.
On le fera plus tard.
Donc je compile.
J'ai une erreur. Voilà.
Donc c'est bon, j'ai mon test.
Donc là, c'est l'occasion de sauver, donc là j'ai mon
package "Dice", je vais le sauver de manière locale, "Save".
J'en avais fait quelques-uns avant pour me chauffer donc
moi je vais en refaire un, qui est "New version with
rolling and test". Très bien.
Voilà, on a sauvé.
Donc maintenant ce que j'aimerais faire, c'est changer un
peu l'interface de création. Déjà on va
recatégoriser, si on veut charger un peu l'interface de
création, on aimerait dire par exemple: "to create a die
use die faces".
Donc là ce qu'il faut voir sur cette expression, c'est
que faces c'est un message envoyé à la classe die et pas
à une instance de la classe die, comme c'est le cas dans
la méthode roll ou dans les autres méthodes qu'on a codées jusqu'à présent.
Donc on va faire ça pour que vous compreniez bien quand
est-ce qu'on doit utiliser et aller au niveau class ou pas.
Donc commençons par écrire un test.
Donc "betterInterface".
Donc si je reprends la même logique,
"TestbetterCreationInterface", ce
que j'aimerais faire ici c'est faire quelque chose comme ça, par exemple.
Et que ça, ça soit faces.
Ce que je vais faire je vais le faire lentement.
Là, je viens ici et je fais "instance creation" faces anInteger.
Donc je pourrais l'écrire de façon compacte mais là je
vais faire d'une manière calme.
Donc je crée un die.
Donc je fais "self new", puisque self là c'est la classe die elle-même.
Donc je lui demande: crée-moi une instance.
Et maintenant, à cette nouvelle instance je vais utiliser
un accesseur pour pouvoir lui mettre la valeur qui a été passée en argument.
Et bien sûr je vais rendre le dé que je viens de créer.
Donc quand je vais exécuter le code, ça va pas marcher parce
que faces n'existe pas, donc il ne faut pas s'inquiéter.
Ce qu'on voit, c'est que mon test ne passe pas, mais que
c'est normal, parce que si j'exécute même ça par
exemple, si je fais débug pour voir.
Donc si je fais Over, là il me dit: oui, je ne connais
pas le message faces.
Donc là on va le faire calmement, je ne vais pas le faire dans le débugger.
Je vais dire: oui c'est vrai qu'il fallait qu'ici j'ajoute
un accesseur. Donc je vais faire faces: anInteger.
Et là on va faire faces : anInteger.
Et tant qu'à faire, je vais faire l'accesseur en lecture.
Voilà, je rends celui-là.
Et là maintenant mon test est vert.
Donc on va sauver, on va en profiter pour sauver, "save
better die creation method with tests".
Très bien.
Donc maintenant ce qu'on va pouvoir commencer à faire, on
va pouvoir commencer à définir ce qu'on veut pour
diceHandle. Donc en gros, si on regarde, diceHandle,
comment on aimerait l'écrire ?
On aimerait écrire diceHandle new addDie.
Donc là on va créer un nouveau dé, "die faces 6, addDie".
"Die faces 10".
Donc ce qu'on va faire, on va commencer par écrire une
classe de test, cette fois-ci.
Donc nouvelle classe qui hérite de TestCase,
très bien. Donc j'ai ma nouvelle classe de tests.
Et là, je vais définir un test.
L'idée c'est que je vais créer une poignée et puis je
vais vérifier que j'ai les bons dés dedans.
Donc je vais faire "testAdding", je veux réutiliser mon
code, il n'y a pas de raison.
Donc, j'ai mon
handle ; yourself parce que je veux
récupérer le receveur du message, c'est-à-dire la poignée
et pas l'argument qui est ici.
Et maintenant, qu'est-ce que je dois faire?
Je dois faire "self assert h diceNumber
equals 2".
Je compile. Alors bien sûr le système me dit: je ne
connais pas la variable DiceHandle, est-ce que tu veux
que ça soit une classe ?
Ben oui. Il faut que ça soit une classe.
Donc là, il va me la définir.
Comme je sais que de toute façon il va falloir que je
stoppe mes dés, je vais en profiter pour
mettre une variable d'instance.
Je compile tout ça.
Donc maintenant, c'est rouge parce que "Add die" n'a jamais été défini.
Donc on va le faire.
Avant de faire ça, ça serait bien qu'on initialise
la poignée, donc on va le faire comme ça, ça nous évitera
d'avoir un bug plus tard.
Donc dice : = OrdereCollection new.
Recategorize.
Et maintenant, je dois être capable de faire tourner mon
test, qui va planter. Voilà, très bien.
Et je vais créer Add die.
Adding.
Donc là, il me dit: oui, il faudrait que tu implémentes cette méthode.
Oui, ça fait du sens.
Donc là, je vais faire "Dice add adie".
Voilà, très bien. Donc mon test ne va pas toujours
marcher parce que je n'ai toujours pas défini la méthode
diceNumber mais faisons-le. Oui, diceNumber, on va la
créer, très bien, dans accessing cette fois-ci.
Et diceNumber, qu'est-ce que ça va faire?
Ça doit rendre finalement
dice size. Voilà, je recompile, proceed.
Et mon test doit être vert donc j'en profite pareil, mes
tests sont verts super, je prends et je sauve.
"With addDie and test".
Donc le test on pourrait l'améliorer parce que là,
finalement, il teste qu'on ajoute deux nombres, moi je voudrais
qu'on teste quand
on ajoute 2 fois le même dé, on ne le perd pas.
Donc je vais faire "TestAddingTwiceTheSame DieisOK".
Donc là, qu'est-ce que je vais faire?
J'ajoute 6 et 6 et je dois bien obtenir 2.
Donc je fais ça, je fais tourner mon test, c'est vert, super.
Donc maintenant, ce qui serait sympa, c'est que on puisse
définir ce que c'est que de faire
ajouter 2 dés. Mais avant ça, on va faire quelque chose.
Si vous regardez, moi ce que je n'aime pas trop c'est que
quand j'inspecte ça, par exemple,
si je fais" Inspect" là-dessus, je ne vois
pas les valeurs des dés et puis ce n'est pas pratique pour débugger.
Dans le débugger, on ne voit pas ça.
Donc d'abord, avant de continuer, je voudrais améliorer
ça donc ce que je vais faire je vais ajouter une méthode
dans le protocole Printing. Donc la méthode "PrintOn",
elle est définie sur tous les objets du système et elle
va permettre de convertir un objet dans une
représentation textuelle et
elle nous passe une stream et nous, on va mettre juste la
représentation qu'on veut dedans.
Donc si je fais ça, je n'ai rien fait en fait.
Si je fais super PrintOn en fait, je n'ai rien fait.
Maintenant ce que je vais faire, je vais faire
"aStreamnextPut", donc je vais mettre des caractères dans
la stream, mais qu'est-ce que je vais commencer par mettre?
Je vais mettre une parenthèse avec un espace, ça sera
peut-être plus joli, une parenthèse.
Après je vais prendre les faces et je vais les convertir
en nombres, en chaînes, et je vais concaténer tout ça,
avec une petite parenthèse fermante.
Donc si je fais ça, j'ai fermé le débugger, donc on va le rouvrir.
J'ai mon débugger. Voilà maintenant, j'ai un dé à 6 faces
et un dé à 10 faces. Donc c'est quand même beaucoup plus
sympa, vous allez voir, si jamais on a des bugs, ça nous aidera.
Donc là je n'ai rien fait de spécial, mes tests tournent.
Je resauve, ça ne coûte pas cher, "With printing".
On va marquer "with die printOn".
Très bien et maintenant on va faire le test, on ne va
pas le faire, on va y aller directement.
On va faire "add protocole roll operations".
Et là, donc
il y a plusieurs façons de définir ça.
Je vous en propose une, ce n'est pas la plus jolie mais
au moins c'est la plus claire probablement pour vous.
Donc il y a des façons très compactes, je pourrais le
faire en une ligne, mais en utilisant des itérateurs
comme "Injected to", là, je vais faire une boucle.
Donc qu'est-ce que je fais ?
Je prends une valeur que j'initialise à zéro.
Après je vais faire une boucle sur tous les dés, et pour
chaque pas de boucle je vais récupérer un dé et ce dé qu'est-ce
que je vais faire ?
Je vais lui demander de me tirer un tirage du dé et d'ajouter
le résultat dans ma variable.
Rien de bien fou mais au moins ça a le mérite d'être très très explicite.
Et donc maintenant, si je fais "Inspect" et que là je
fais "Roll", 5 ça ne me prouve pas que ça marche.
On va essayer encore.
11. Voilà, ça, ça marche.
Donc on va essayer d'écrire un test, il n'y a pas de
raison, donc on va faire "Test", je veux garder celui-là
et je vais l'appeler rolling.
Alors comment on devrait faire pour tester ça?
Il faut que ça soit entre 1 et le maximum du nombre de dés.
Donc, on va faire ça.
On
pourrait définir une méthode qui fait ça.
Faisons une méthode qui définit le maximum.
"Operation maxValue".
Qu'est-ce que c'est que maxValue ?
Finalement, c'est très proche de ça.
C'est que là, au lieu de faire un roll,
je vais demander les faces.
Donc vérifions. Si je fais "Inspect", là
je vais faire "maxValue"16".
Oui c'est juste 10 et 6.
Donc, on va écrire un test tant qu'à faire pour
"maxValue". Donc j'ai ça, je vais faire "maxValue equal
16".
Alors là, vous avez vu, j'aurais pu coder un truc tout
sale dans mon test, mais finalement ça vaut plus la peine
de créer une méthode dans la classe et de l'utiliser.
Donc maintenant, on va pouvoir tester que la méthode roll
fonctionne bien. Donc je vais faire "roll",
et je vais lui dire qu'elle doit être absolument comprise
entre "Roll between 1 and
h maxValue. Voilà.
Donc si je fais ça, ok ça marche.
Ce n'est pas très statistique, donc là on pourrait faire
quelque chose du style: 1 000 timeRepeat.
Ok. Et là,
on a nos 1 000 tests.
Donc là, on va sauver maintenant, c'est très bien.
On va encore sauver, "save" added maxValue
and roll with tests.
Donc maintenant, ce qu'on va faire, on est presque au
bout de nos peines, nous ce qu'on voudrait exprimer c'est
qu'au lieu d'avoir "die faces 6",
moi j'aimerais avoir "1 D6".
Et
ce qu'on voit là finalement c'est que ça serait envoyer
le message dé 6 à un petit entier.
Donc on va aller regarder la classe Integer.
Et ce qu'on va faire, on
va définir une extension de classe.
Qu'est-ce que c'est qu'une extension de classe?
Je vais packager mes méthodes avec le même nom que le package.
Donc vous allez voir, qu'est-ce que je fais, j'ajoute un
protocole, je lui mets *, il doit commencer par *dice qui
est le nom de mon package, automatiquement c'est grisé et
là ça veut dire que cette méthode-là sera packagée en
même temps que ce package. Donc on va le faire.
Donc imaginons qu'on fasse qu'est-ce que c'est que D6?
Un D6. Il faut réfléchir un petit peu.
On va d'abord créer une poignée parce que ça pourrait
être deux D 6 finalement.
Donc "handle", on va faire "diceHandle
new",
très bien donc j'ai créé mon truc.
Maintenant pour chaque receveur, donc je vais faire
"self", ça c'est mon entier, "timesRepeat".
Décidément, on l'aura beaucoup utilisé le timesRepeat, c'est rare.
"TimesRepeat handle addDie", de quoi
? De "die faces". Et là,
on le sait, c'est 6.
Et bien sûr ce serait peut-être bien que je rende la poignée.
Donc est-ce que ça marche ?
Donc on va tester comme ça et puis après on va écrire un test.
Mais si je fais 2 D 6 Inspect, voyez j'ai
bien 2 D 6. Donc, c'est sympa.
Donc écrivons un test.
On va les catégoriser ces tests quand même.
Donc on va faire "testNewSyntax".
Alors là, pour le moment, on n'a que D6, après on va généraliser.
Mais donc nous ce qu'on veut faire, c'est exactement la
même chose que ça, donc on va avoir une handle, on va dire 2 D 6.
Et là, ce qu'on va faire, on va faire "selfAssert".
Donc qu'est-ce qu'on pourrait tester?
Que diceNumber equals 2,
par exemple. Alors vous avez vu, des fois j'utilise
diceHandle, j'aurais pu aussi utiliser = 2 ici.
En général, c'est plus sympa d'utiliser assert equal
parce que comme ça, quand il y a une erreur, le système
me dit: j'ai reçu ça et j'ai eu cette valeur à la place.
Si je mets =, il va dire: j'ai eu une expression fausse.
Donc là, pour l'utilisateur terminal, qui est vous en
fait, en tant que développeur, c'est plus sympa d'utiliser
assert equals parce qu'il va dire: j'ai reçu 3 alors que
j'attendais 2, par exemple.
Donc là, si je fais ça.
Voilà, oui ça marche.
Alors est-ce qu'on pourrait avoir une version un peu plus
intelligente du test ?
Pour le moment, ça nous convient.
Ce qu'on va faire et donc là, vous voyez maintenant dans
le package, j'ai une extension qui est D6.
Donc on va généraliser ça avec "a number until", a
number of faces.
Et donc "a number of faces", on va le mettre ici.
Et donc on va récrire D6, parce que ce serait quand même plus sympa D6.
On va l'écrire comme ça D6, on va
faire "^ self D6".
Donc on va faire tous les autres, il n'y a pas de raison.
On va faire 4,
2. C'est plutôt une pièce qu'un dé mais 2,
10 et 20.
Vous avez compris le principe.
Donc faisons tourner nos tests, puisque on a changé d'implémentation
2 D6, donc ça veut dire que ça marche.
Donc, on va sauver.
Donc maintenant que nous reste-t-il à faire?
En fait, il nous reste à pouvoir additionner des poignées.
Donc qu'est-ce que j'ai envie d'avoir comme test?
J'ai envie de m'assurer que si je fais, par exemple,
"addingHandles", j'ai envie de m'
assurer que si, en plus je peux utiliser une nouvelle syntaxe.
Donc là, c'est sympa.
Ce que j'ai envie de faire, j'ai envie de faire que si je
fais 2 D 20 + 3 D5
D 6 tant qu'à faire, on ne va pas commencer à compliquer
les choses. Je dois avoir combien ?
diceNumber doit être égal à 5.
Donc là ce que vous voyez, c'est qu'on va devoir définir l'opérateur +.
Donc dans Pharo, + ce n'est pas un opérateur, c'est juste un message.
Donc on va définir un message sur la classe poignée de dé.
Donc on va mettre plus.
Alors "adiceHandle".
Donc maintenant la question qu'on peut se poser c'est:
est-ce qu'on modifie le receveur ou est-ce qu'on fait une
approche fonctionnelle ?
Moi je préfère faire une approche fonctionnelle dans
laquelle on recrée une nouvelle poignée.
Donc je vais créer une nouvelle poignée, je vais faire
"handle self class new".
Alors là, j'ai évité de mettre diceHandle et il y a une
leçon qui vous expliquera pourquoi plus tard.
Moi je préfère, c'est beaucoup plus proche.
En général, on ne code pas en dur le nom des classes.
Donc vous verrez dans la semaine 7, ou quelque chose
comme ça, il y a toute une explication.
Donc si je fais "self dice do", ce que je vais
faire je vais itérer sur mes dés et je vais les ajouter dans handle.
Donc, je vais faire "handle addDie each",
et je vais faire la même chose… Alors là, je n'ai pas
besoin de self et d'ailleurs, il ne connaît pas le
message, c'est ce qu'il était en train de me dire, et ça
me fait remarquer qu'effectivement je ne l'ai pas défini
et que ça n'a pas marché pour "diceHandle", mais ce
n'est pas grave, compilons d'abord et on fixera après.
Donc là, qu'est-ce que ça veut dire ?
Ça veut dire qu'il me manque un accesseur qui est dice.
Donc on va ajouter dice ici, dice qui me rend la
collection de mes dés.
Voilà. Donc maintenant je vais tester si mon test passe.
Mon test passe, donc c'est super, ça veut dire que j'ai
pratiquement fini, je vais sauver, "with handles
additions". Ok, très bien.
Donc voilà, ça veut dire que maintenant on peut écrire
2 D 4 et qu'on
puisse faire "Roll", et que ça nous rende un nombre.
Donc maintenant, vous êtes prêts à jouer à "Donjons et Dragons".
Donc ce qu'il faut voir, finalement c'est que on a défini
nos méthodes, on a défini nos tests, on les a fait
tourner, on a été étendre une classe du système, la
classe "entiers", avec des extensions qui sont liées à
notre package, qui ne seront visibles que quand notre package sera chargé.
On a aussi fait de la surcharge d'opérateurs, mais en
fait on a défini juste un nouveau message plus, parce que
dans Pharo l'addition est juste un message comme les
autres, ce qui nous a permis d'exprimer relativement facilement un joli DSL.
Donc à vous de jouer !