http://tddbc.doorkeeper.jp TDD Boot Camp 2013-07 -- TDDBC で、偶然にもロンドンから来日していたSteve Freeman氏を招くことができた。ちなみに本当に偶然の来日で、その日の夕方にご家族と隅田川の花火を見る予定だったらしい。貴重な時間である。
20分ほど講演していただき、さらに参加者と一緒にペアプロ課題に挑戦してもらった。しかもペアプロでっていう貴重な体験をさせてもらったので、そのことについてまとめたい。
Steve Freeman氏は書籍 "Growing Object-Oriented Software, Guided by Tests" (邦訳「実戦テスト駆動開発」)の共著者の一人で、Javaのモックフレームワーク "JMock"の開発者の一人。当然、自動販売機の課題にもJMockを駆使してモデリングしていただくことになった。
実際にTDDを体験してもらうために、TDDBCでは短い課題を用意している。そのときは自動販売機の実装だった。しかし、そもそも半日で終わるような小さいものではない。なので、ある程度の道筋を示すべくいくつかのstepを用意している (http://devtesting.jp/tddbc/?TDDBC%E5%A4%A7%E9%98%AA2.0%2F%E8%AA%B2%E9%A1%8C, https://github.com/maikeru/vending_machine )。step 0は「お金の投入と払い戻し」とされている。
ところが、まずここに物言いがついた。「これはこの通りやらなきゃいけないのか?」。
氏曰く、そもそも自動販売機に求めることは商品を提供することにある。なので、お金の払い戻しはおそらく必要になるだろうが、最も欲しいものではない。欲しいのは、お金を入れたら商品が提供されることである。
そこで、いくつかやりたいことをA4の紙にペンで書いていくところから始まった。文字起こしは https://github.com/tddbc/jpy_tdd/blob/master/todo.txt 。単純なケースから複雑なケースへ。この程度ピックアップすれば当面の仕事は十分だろう、みたいな量だけ。一種のユーザーストーリーですね。
次の疑問は、これをどうやって「オブジェクト同士の協調」としてモデリングしていくかだった。
自動販売機オブジェクトを考える。そこには在庫として100円の水が5本がある。100円を投入する。その水を選ぶ。ゴトンと水が提供される。
1つの実装例は、こんな感じになるだろう。これを「手続きモデル」と呼ぶことにする:
VendingMachine machine = new VendingMachine();
machine.setStock(5);
machine.acceptPayment(100);
int vendedAmount = machine.vend();
このとき、vendedAmountが1であることを確認すればよい。
assertThat(vendedAmount, is(1));
もう一つの例は、水の提供を外部インタフェースとして定義するやり方だ。
自販機の提供先としてdispenserオブジェクトを考える。自動販売機の中にある、ペットボトルが落ちないように支えてるロックを解除して取り出し口にゴトンする機械を操作するヤツ、みたいなものと考えればよい。その動作を実現するためのdispenseメソッドを用意して、それを1回呼んだらゴトンと落とすことにしよう。これは自販機自体が備えていると考えるのが妥当だ。
これは「オブジェクト協調モデル」とでも呼んでみよう。
interface Dispenser {
void dispense();
}
で、自販機はこうなるだろう:
VendingMachine machine = new VendingMachine(dispenser);
machine.setStock(5);
machine.acceptPayment(100);
machine.vend();
machine.vend()の中で dispenser.dispense()が1回呼ばれ、実際の機械ではゴトンと落とすわけだ。この部分、dispenser.dispenseが1回だけ呼ばれていることをテストするために、たとえばこんな実装を用意する:
class MockDispenser implements Dispenser {
int amount = 0;
public void dispense() {
this.amount += 1;
}
}
Dispenser dispenser = new MockDispenser();
VendingMachine machine = new VendingMachine(dispenser);
machine.setStock(5);
machine.acceptPayment(100);
machine.vend();
assertThat(dispenser.amount, is(1));
そんなモッククラスをいちいち実装せずとも良い感じにテストするためのツール、それがモックフレームワークになる。続きは https://github.com/tddbc/jpy_tdd/blob/master/src/jpytdd/VendingMachineTests.java を参照のこと。氏自らのコードです。モデリングが若干違うのはご容赦ください。
手続きモデルは、machineインスタンスはいくつかのvoidメソッドと、intを返すvendメソッドからなる。一方でオブジェクト協調モデルは、全てのメソッドは返却値を持たない。投げっぱなし。その先のインタフェースが何かをすることになる。
もしかして、エクストリームなオブジェクト協調モデルでは原則、全てのオブジェクトの全てのメソッドは値を返却しないことになるかもしれない。自身の状態を変えるか、もしくは、他のオブジェクトに何かを作用を及ぼすか。まぁただ、それはそれでかえってややこしくなると思うので、実際には書籍「ドメイン駆動設計」でもおなじみの値オブジェクトを組み合わせて、良い感じに使っていくことになるだろう。ちなみに値オブジェクトは単純な入出力関係を持つだけなので、テストではモックの出番はない。
もう一つのパラダイムである関数型モデルは、実は手続きモデルに近い。HaskellとかよーわからんのでとりあえずSchemeでそれっぽく書くと:
(define (make-machine stock-amount)
… )
(define (stock-amount machine)
… )
(define (vend machine)
(list 1 (make-machine (- (stock-amount machine) 1))))
みたいな感じになるだろう。たぶん。
直感的なほうを選べばよい。私はこれくらいしか言えない。
より広くテストしたい場合に、外部インタフェースの定義のほうが適しているような気もする。その反面、複雑になる心配がある。広いテストは外部から叩くようなテストを用意すれば十分かもしれない。混在させてしまうと、読み手の理解を妨げるかもしれない。
ということでみなさん、いろいろ試してみてください。
- モック派と原理派(もしくはロンドン学派とデトロイト学派)
- 「mockに対するFUDが多かったからGOOS本を書いた」
- コラボレートするのはオブジェクト
- クラス指向の是非は別の話
- オブジェクトの大きさの話はどうだったっけか
- Userインスタンスとか付けたくなったら、オブジェクト協調モデルを作りたいのかも
- 名前付けは難しい
- IDEのrenameに頼る
- 「多言語を混ぜる、日本語メソッドを使うというのは面白い」らしい
- 設計の迷いと、何をどこまで考えれば十分か?って難しいよね
- 「コアモデルとインタフェースのドーナツ型の図いいよね」言ったら、「そうだろ、これに気づくのに何年もかかった」みたいなことを言っていた
どうでもいいツッコミですが、2012-07ですか?2013-07じゃないでしょうか。