今日は。 SPEEDA を開発している濱口です。
前回の続きです。
以下の通り、今回も設計の段階的な進化に沿った忠実な写経ができたと思います。
概要としては、依存関係でがんじがらめになった設計を、ダックタイプを使って柔軟性のあるものに変える、というものです。
ハイライトだけ抜粋します。
↓これが、
"依存しまくりの恐ろしい分岐" Trip >> prepare: preparers [ preparers do: [ :preparer | preparer class == Mechanic ifTrue: [ preparer prepareBicycles: bicycles ]. preparer class == TripCoordinator ifTrue: [ preparer buyFood: customers ]. preparer class == Driver ifTrue: [ preparer gasUp: vehicle; fillWaterTank: vehicle ] ] ]
↓こうなる。
"Preparer という抽象に任す" Trip >> prepare: preparers [ preparers do: [ :aPreparer | aPreparer prepareTrip: self ] ]
ただし、以下のような記述に引っかかります。
ダックタイプをつくるときは、そのパブリックインターフェースの文書化とテストを、両方ともしなければなりません。
幸い、優れたテストは最高の文書でもあります。ですから、すでに半分は終わっているようなものでしょう。
あとはテストを書きさえすれば、両方とも終わりです。
ダックタイプのテストについてのさらなる話は、「第9章 費用対効果の高いテストを設計する」を参照してください。
このハンズオンは TDD で行っている為、テストのことを先送りにはできません。
既にテストは書いていましたが、
私の書いたテストは妥当かどうか、著者のものと比較してみました。
ダックタイプのテスト要件
「9.5 ダックタイプをテストする」で、著者は以下のように主張しています。
テストで記述すべきことは Preparer ロールの存在であり、証明するべきことは、ロールの担い手がそれぞれが正しく振る舞い、 Trip がそれらと適切に協力することです。
他の箇所も記述も含めまとめると、ダックタイプのテスト要件としては以下の 3 つになると解釈しました。
- ロール(ダックタイプ)が存在することを可視化する
- メッセージ受信側がロールを担っていることを証明する
- 送信側がメッセージを送っていることを証明する
それぞれ対応する Ruby のテストコードは以下です。
# 1. ロール(ダックタイプ)が存在することを可視化する module PreparerInterfaceTest def test_implements_the_preparer_interface assert_respond_to(@ object, :prepare_trip) end end
# 2. メッセージ受信側がロールを担っていることを証明する class MechanicTest < MiniTest::Unit::TestCase include PreparerInterfaceTest def setup @mechanic = @object = Mechanic.new end # @mechanic に依存するほかのテスト end class TripCoordinatorTest < MiniTest::Unit::TestCase include PreparerInterfaceTest def setup @trip_coordinator = @object = TripCoordinator.new end end class DriverTest < MiniTest::Unit::TestCase include PreparerInterfaceTest def setup @driver = @object = Driver.new end end
# 3. 送信側がメッセージを送っていることを証明する class TripTest < MiniTest::Unit::TestCase def test_requests_trip_preparation @preparer = MiniTest::Mock.new @trip = Trip.new @preparer.expect(:prepare_trip, nil, [@trip]) @trip.prepare([@preparer]) @preparer.verify end end
それを知らずに書いた私のテスト
一方、私が 9 章を読む前に書いていたテストは以下です。
"受信側" MechanicTest >> testPrepareTrip [ | mechanic trip | mechanic := Mechanic new. mechanic stub. trip := Mock new. trip stub bicycles willReturn: #(#bicycle1 #bicycle2). mechanic prepareTrip: trip. (mechanic should receive prepareBicycle: #bicycle1) once. (mechanic should receive prepareBicycle: #bicycle2) once ] TripCoodinatorTest >> testPrepareTrip [ | tripCoordinator trip | tripCoordinator := TripCoordinator new. tripCoordinator stub. trip := Mock new. trip stub customers willReturn: #customers. tripCoordinator prepareTrip: trip. tripCoordinator should receive buyFood: #customers ] DriverTest >> testPrepareTrip [ | driver trip | driver := Driver new. driver stub. trip := Mock new. trip stub vehicle willReturn: #vehicle. driver prepareTrip: trip. driver should receive gasUp: #vehicle. driver should receive fillWaterTank: #vehicle ]
「2. メッセージ受信側がロールを担っていることを証明する」ことは、出来ています。
ロールを担っている( prepareTrip を受信できる)ことに加え、受信したら何をするかも合わせてテスト出来ていて良いのではないでしょうか。
"送信側" TripTest >> testPrepare [ | trip preparers preparer1 preparer2 | trip := Trip new. preparer1 := Mock new. preparer2 := Mock new. preparers := { preparer1 . preparer2 }. trip prepare: preparers. (preparer1 should receive prepareTrip: trip) once. (preparer2 should receive prepareTrip: trip) once. ]
「3. 送信側がメッセージを送っていることを証明する」ことも出来ています。
ここは Ruby のテストとほぼ同じですね。
残るひとつ、「1. ロール(ダックタイプ)が存在することを可視化する」は出来ていませんでした。
テストカバレッジを上げてみる
比較して足りなかったテストが本当に必要なのかということは置いておいて、
このハンズオンでは忠実な写経を旨としているためお手本と同じカバレッジをとにかく満たすようにします。
キモは受信側でなるべくテストコードを共有することです。
Ruby では module を使ってそれを実現しています。
Smalltalk では trait を使って実現しました。
TPrepareInterfaceTest >> targetObject [ ^ self SubclassResponsibility ] TPrepareInterfaceTest >> testImplementsThePreparerInterface [ self assert: (self targetObject respondsTo: #prepareTrip:) equals: true. ]
これを以下のように targetObject フックメソッドを実装して使用します。
MechanicTest >> targetObject [ ^ Mechanic new ] TripCoodinatorTest >> targetObject [ ^ TripCoordinator new ] DriverTest >> targetObject [ ^ Driver new ]
結論
実のところ、上記の体験を経ても次に同じようなケースに遭遇した時に、ダックタイプを可視化するテストを書けるかまだ自信はありません。
可視化のメリットをまだ理解出来ていないからだと思います。
今は、コードではなくテストコードでダックタイプを可視化することで、コードの柔軟性を殺さずに必要なコミュニケーションを行っている、ということだとなんとなく理解しています。