このビデオは本MOOCでの反復です。
ここでの反復というのは 自分でやってみることです。
ビデオを見て見たものを 1つ1つ実行することです。
我々がプログラムする方法を見ることを お勧めする意味もあります。
台本通りに忠実には進まないかもしれませんが
できるだけ、そうしましょう。 しかしバグを見つけたり
そんなことがあったら その都度対処しましょう。
このビデオでは 簡単な言語をプログラムします。
ロールプレイングゲームにあるような 感じの言語です。
ダンジョンズ＆ドラゴンズのような。 例えば
この記述を手にした人は
どういう風に読み取ったらいいのでしょう？
こういう意味です。 2つの20面ダイスと1つの4面ダイスを振りなさい。
これが Pharo の表現式になることが おわかりでしょうか。
つまり、このビデオでは
ダイスを表現するクラスと
ダイスのハンドルを表現するクラスを 実装します。
では始めましょう。
始めに、パッケージを定義します。
Dice と呼ぶことにします。
他のものは見たくないので
このビデオでは
コードをデバッガーで書くやり方は しないつもりです。
ほかのビデオでお見せした方法ですが
ケースバイケースで、そうすべき機会があれば そうすることもあるでしょう。
ここで、Die クラスを定義します。
面の数 faces を持っています。
コンパイルします。 クラスコメントを追加します。
当面は大したことはありません。 とてもとても単純なものですから。
このオブジェクトの初期化を
作る作業を始めてみましょう。
こんな風にやります。
スーパークラスの初期化を呼んで
面の数のデフォルト値として6を代入します。
それが最も一般的なダイスなので。
ここでテストクラスを作り始めます。
一旦作ったものを
壊してしまわないようにするためです。
テストクラスは TestCaseクラスのサブクラスです。
DieTest クラスとします。
これでテストクラスができました。
最初にやるべきテストの1つとして
いつもそうする必要はありませんが
ちゃんと動くテストから始めたいのです。
今の所、あまり多くのものはないので 初期化はOKだと言いたいです。
ここでお見せするのは
例外が出てくることを確認するテストや
例外が出ないことを確認するテストの 例にもなっています。
では書いてみます。
Die new は Error を投げてはいけない。
どういう意味でしょう？
「Die new というコードを実行した時に
エラーが発生してはいけない」 という意味です。
このテストを分類して実行します。
グリーンです。ばっちりです。
ではここで
ダイスを振るメソッドを定義します。
Pharo には
atRandom というメソッドがあります。
atRandomは何をするのでしょうか？
そのメソッドの機能は…そうですね
実装を見て確認してみましょう。
atRandomは何をするのでしょう？
それは 1 から self までの ランダムな整数を返します。ピッタリですね。
なので、新しいメソッドを
operationsに定義します。
何をしましょうか？
roll（転がす）です。
「ランダムな出目を返す」ということです。
そこでこのメソッドのテストを書きます。
testRolling 何をするのでしょう？
ダイスを作ります。
d:= Die new
そして 1000 timesRepeat:
何を？ d roll
そしてこれが 1 から 6 の間であってほしいのです。
between: and: いいですね。
between: 1 and: 6
これはあまり良くないです。 というのも、ここでは
6面ダイスのテストしか書いていません。
ダイスの面の数に従って動くべきです。
それは後回しにしましょう。
なのでコンパイルします。
エラーになりました。こうです。
これでテストができました。
ここで保存しましょう。 Dice パッケージがあるので
ローカルに保存します。 Save です。
練習で他のものをいくつか作ったので
新しいものを作ります。 「新しいバージョン
ダイスを振るのとテスト」 これで良し、と。
はい、これで保存されました。
ここで、インスタンス生成の インターフェイスを変えようと思います。
まずはカテゴリーを整理します。
インスタンス生成を少し変えたい。
ダイスを作るときに
面の数をつかいたい。
もう解ったはずですが この式の中で
faces は Die クラスへ 送られたメッセージであって
Die クラスのインスタンスへの メッセージでははありません。
インスタンスへのメッセージは今のところ roll メソッドかまたは別のクラスのメソッドだけです。
これで理解してほしいのは
いつクラスレベルへ行くべきか、あるいは 行くべきでないか、ということです。
まずはテストを書きましょう。
betterInterface
同じロジックで続けると
testBetterCreationInterface
ここで例えばこんなことをしたいのです。
そしてこれは faces です。
ゆっくりやります。
ここに行ってinstance creation と入力して faces: anInteger
短く書くこともできますが ここでは
「穏やかな」やり方をします。
ダイスを作ります。
self new と書きます。 ここでの self は Die クラス自身です。
インスタンスを作る と伝えます。
そして このインスタンスに対して
アクセサーを使って 引数として渡された値を代入します。
当然、今まさに作ったダイスを返します。
このコードが実行して ちゃんと動きません。
faces はまだ存在していないからです。 なので心配しなくていいです。
テストが OK ではないですが
正常なことです。これを実行すれば execute this
例えば、デバッグして見てみれば
Over をクリックすると
faces というメッセージがわからない と言ってきます。
ここでは「穏やかな」やり方でいくので デバッガではやりません。
その通りで アクセサーを追加する必要があります。
faces: anInteger と書いて
faces := anInteger と書きます。
ついでに読み出しのアクセサーを書きます。
これを返します。
これでテストはグリーンです。
なので保存します。 save
ダイスのより良い生成メソッドと テスト
よしと。
これで
ダイスハンドルの定義を 始めることができます。
ダイスハンドルについて
どんな風に書きたいでしょうか？
DiceHandle new addDie: みたいに書きたいと思います。
そして新しいダイスを作るわけです。 die faces: 6; addDie:
Die faces: 10
今回は
テストクラスから書き始めたいと思います。
なので TestCase から継承した新しいクラスです。
よし。 新しいテストクラスができました。
そしてテストを定義します。
ハンドルを作って
正しいダイスが入っているか チェックします。
testAdding コードを再利用します。
そうしない理由はないですよね。
なのでここで
handle さらに yourself
メッセージレシーバーが欲しいので。 つまりハンドルであって
渡した引数ではなく。
さあ、何をしたらいいでしょう？
self assert: [h diceNumber
equals 2
コンパイルします。 明らかなことですが、システムが
変数 DiceHandle を知らないと言っています。
そういうクラスが欲しいのですか？と。
はい、クラスです。
これで定義されます。
いずれダイスを持たないといけないことは もう分かっていることなので
ちょうどいい機会なので
ここでインスタンス変数を入れます。
これをコンパイルします。
これで（テストは）レッドです。 addDie: が定義されていないからです。
なので定義しましょう。
その前に ハンドルを初期化しておいたほうがいいですね。
こんな風にします。
後々のバグを予防できます。
dice : = OrderedCollection new.
Recategorize（分類し直し）
そして、これでテストできるはずです。
で、クラッシュします。 はいはい、大丈夫です。
addDie: を作ります。
adding です。
このメソッドを実装しなければならない と言ってきます。
まあその通りです。
dice add: aDie と書きます。
はい、ばっちりです。 テストはまだ動きません。
diceNumberをまだ定義していないからです。
では定義しましょう。 はい、diceNumber です。
今回は accessing に定義します。
そして diceNumber ですが 何をしましょうか？
返さないといけないのは
ダイスの個数です。またコンパイルします。 テストを進めるとグリーンになるはずです。
テストはグリーンになったので
保存します。
addDie とテスト。
テストを改良することもできます。
ここでは 2つのダイスをくわえていますが
同じダイスを2回追加しても
大丈夫かチェックします。
testAddingTwiceTheSameDieIsOK と。
ここで何をしましょう？
6面ダイスと6面ダイスを足したら ダイスは2つになるはずです。
これでテストを実行します。 グリーンです。素晴らしい。
さて、ここで
2 つのダイスを追加することを定義できたら
よいのですが、その前に やっておくことがあります。
見てください。気に入らないのは
例えばこれをインスペクトすると
ダイスの中身がわかりません。
それでデバッグするのは実践的ではないですね。
デバッガーではこれが見えないのです。
なので、先に進める前に これを改良しておきたいのです。
printing プロトコルに メソッドを追加します。
printOn: メソッドです。
システムの全てのオブジェクトに 定義されているメソッドで
オブジェクトを
テキスト表現に変換して
ストリームに渡します。
今回は表現をより詳細にします。
こうすると何もしないのと同じです。
super printOn: をすると 前と何も変わりません。
そしてさらに
aStream nextPutAll: で文字をストリームに入れていきます。 put characters in the
まず何を入れましょう？
空白と括弧を書きます。
このほうが良い感じでしょう。
そして faces をどうするかですが
数値を文字列に変換して 全部を連結して
括弧を閉じます。
こうして、インスペクターを閉じて もう一度開きます。
6面ダイスと
10面ダイスがあります。 much
見ての通り、前よりも良いです。 バグが出てきた時に、きっと手助けになるでしょう。
大したことは何もしていないので テストは動いています。
また保存します。大した手間ではありません。 表示、と。
Die の printOn: と。
これでよし。ではテストを作りましょう。
いや、テストを作るのではなく 直接やりましょう。
operations プロトコルで
さて
これを定義する方法はいくつかあります。
そのうち1つをお見せします。 一番良い方法というわけではありませんが
おそらく一番解りやすい方法です。
コンパクトに
1行で書くこともできますが
inject:into:のようなイテレーターを使います。 今回はループを使います。
では何をしましょうか？
0 で初期化された変数を使って
全てのダイスについてループして
ループのそれぞれの周回で ダイスを取り出して
このダイスで何をしましょう？
ダイスを振って 結果を変数に
足していきます。
何も大したことのないですが とても、とても明示的です。
ここでインスペクトして
roll します。5 です。 これだけでは動いている証明にはなりません。
もう一度やってみます。
11 です。いいでしょう、動いています。
テストを書いてみます。
そうしない理由はないので テストします。
これを使って ここは Rolling としましょう。
では、これをどうテストしましょうか？
1からダイスの最大値の間のはずです。 It has to be between one and the maximum of the number of dice.
なので、こうしましょう。 So we will do this.
これをする We
メソッドを定義できますね。 could define a method doing this.
最大値を求めるメソッドを作りましょう。 Let's create a method defining the maximum.
operations の maxValue です。 "Operation maxValue".
最大値とは何でしょう？
これに似ているはずです。 It is very close to this.
ここではダイスを「振る」かわりに
面の数を訊きます。 I will ask for the faces.
ではチェックしてみましょう。 インスペクトして Let's check. If I do "Inspect", there
maxValue します。16 です。 I do "maxValue", "16".
はい、10 と 6 なので正しいです。 Yes, it's right 10 and 6.
では maxVallue のテストを書きましょう。
これを使って maxValue は
16 になるはずです。
見ての通り テストではとても汚いコードを書きましたが
最終的には
メソッドを書いて使った方が良いです。
ではここで roll メソッドのテストに戻ります。
roll をします。
そしてその結果は
1 から h maxValue の間
でなければなりません。
実行すると、はい、動きます。
あまり統計的な根拠はないですが This is not very statistical, so here we could do
something like 1000 timesRepeat: のようなこともできます。
オッケーです。これで
1000回テストができました。
では保存します。素晴らしい。
maxValue と
rollとテストを追加。
これでほとんど完成です。
表現として
Die faces: 6 ではなく
1 D6 のように表現したいと思います。
そして
これは詰まるところ
整数オブジェクトにメッセージ D6 を送る ということです。
なので、Integerクラスを見てみましょう。
これからやろうとしていることは
クラスへの拡張を定義することです。
クラスへの拡張とは？
メソッドをパッケージと同じ名前で パッケージングするのですが
これからお見せします。
プロトコルを追加します。 先頭に * を付けます。
*Dice とすることで 灰色の表示になります。
これはこのメソッドは このパッケージといっしょに
パッケージされるということです。 やってみましょう。
想像してみましょう。 D6とは何でしょう？
D6... ちょっと考えなければなりません。
まずはハンドルを作ります。
2 D6 とかの場合もありますから。
なので、handle と。
DiceHandle new
はい、これでできました。
これで、レシーバー回数ということで I will do
self つまり自分の整数で timesRepeat:
timesRepeat: を本当に多く使いましたが こんなことは珍しいです。
timesRepeat: [handle addDie: で、何でしょう。
Die faces: そして
6 ですよね。
これでハンドルを返せば良さそうですね。
さあ動くでしょうか？
こんな風にテストをして テストを書きます。
しかし 2 D6 インスペクトすると、ほら
2つの D6 が得られます。すごい。
テストを書きましょう。
これらのテストを分類しておきます。
testNewSyntax と書きます。
ここで今は D6 しかありません。 後で一般化します。
これと全く同じことをしたいわけです。
そしてハンドルが得られるはず。 では、2 D6 として
self assert: と。
何をテストしましょう？
diceNumber は 2 のはずです。
お気付きかもしれませんが
ここは = 2 とすることもできます。
一般には、assert:equals: を使う方が良いです。
というのも、このように エラーになった時に
「これを期待していたのに、これが得られた」 とシステムが言ってくれます。
= を使うと、システムは 「間違ってます」としか言えません。
この場合には
開発者としてはassert:equals:を使った方が 良いです。
「2 を期待していましたが、3 が得られました」
のようになるので。
ここは、こうします。
はい、動きました。
もっと賢い方法で
このテストができるでしょうか？
今のところ、これで十分です。
今、このパッケージには
D6 という拡張があることがわかります。
これを後で一般化して with
D: aNumberOfFaces とします。
ここが aNumberOfFaces になります。
そして D6 を書き直します。
D6 をこんな風に書きます。 このほうが良いです。
self D: 6 を返します。
他もやってしまいます。
D4
D2。 ダイスというよりコインですね。
D10 と D20.
これが原則です。
ではテストを走らせましょう。 実装を変更したのですから。
2 D6... 動いています。
では保存しましょう。
やり残したことはあるでしょうか？
あります。 ハンドル同士を足し合わせることです。
何をテストしましょうか？
今回確認したいのは ハンドルを足すことなので
testAddingHandles
(ここで新しい表現を使うことができるのは
良いですね)
テストしたいことは
2 D20 + 3 D5
いや、3 D6 にしましょう。 （問題を複雑にしないようにしましょう）
いくつあるでしょうか？
diceNumber は 5 のはずです。
ここで、+ 演算子を定義しなければなりませんね。
Pharo では、+ は演算子ではありません。 単なるメッセージです。
なので DiceHandle クラスにメッセージを定義します。
+ と書きます。
で、 aDiceHandle です。
ここで考えてみましょう。
レシーバー（ハンドル）を書き変える方法と
関数的な方法があります。
関数的な方法のほうが好みです。
関数的な方法では新しいハンドルを作ります。
そこで新しいハンドルを作ります。
handle := self class new
ここでは DiceHandle と書くのを避けました。
後でその理由を説明するレッスンがあります。
それほど先ではありません。
一般的に、クラス名をハードコードすべきではない ということです。
第7週かその辺りで
きちんと説明します。
self dice do: とすると
自分が持つダイスを列挙します。 そしてそれをハンドルに加えていきます。
handle addDie: each
そして同じことをします。
こちらでは self ではなく…
ああ、このメッセージはわかりませんね。 システムがそう言っています。
それで気付いたのですが まだ定義していません。
なので diceHandle にも動かないわけですが
とりあえず、まずはコンパイルしてしまって 後で直しましょう。
どういうことかというと
アクセサーが足りません。diceです。
なのでここで dice を書きましょう。
dice は 自分のダイスのコレクションを返します。
これでテストしてみます。
テストは大丈夫です。素晴らしい。 完成が近いということです。
保存します。 ハンドル同士の足し算
はい、オッケーです。
つまりこれで
2 D4 と書いて
roll つまり振ることができます。 そして数が返ってきます。
さあ、これでダンジョンズ＆ドラゴンズを プレーすることができます。
ここまでやったことは
メソッドを書いて テストを書いて
テストを走らせて システムクラスを拡張しました。
Integerクラスにこのパッケージにリンクした 拡張をつけることで
このパッケージがロードされた時のみ 見えます。
そして演算子のオーバーロードもしました。
実際には単に + メッセージの 新しい定義をしただけです。
Pharo では足し算は単なるメッセージなので。
これによって良い感じのDSLで 記述できるようになりました。
さあ、あなたがコードを書く番です！