Smalltalk かつ TDD で『オブジェクト指向設計実践ガイド』の「第5章 ダックタイピングでコストを削減する」をハンズオンしたら 9章も確認せざる得なかった

今日は。 SPEEDA を開発している濱口です。

前回の続きです。
以下の通り、今回も設計の段階的な進化に沿った忠実な写経ができたと思います。

  1. ダックを見逃す
  2. 問題を悪化させる
  3. ダックを見つける

概要としては、依存関係でがんじがらめになった設計を、ダックタイプを使って柔軟性のあるものに変える、というものです。
ハイライトだけ抜粋します。
↓これが、

"依存しまくりの恐ろしい分岐"
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 つになると解釈しました。

  1. ロール(ダックタイプ)が存在することを可視化する
  2. メッセージ受信側がロールを担っていることを証明する
  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 
]

結論

実のところ、上記の体験を経ても次に同じようなケースに遭遇した時に、ダックタイプを可視化するテストを書けるかまだ自信はありません。
可視化のメリットをまだ理解出来ていないからだと思います。
今は、コードではなくテストコードでダックタイプを可視化することで、コードの柔軟性を殺さずに必要なコミュニケーションを行っている、ということだとなんとなく理解しています。

© Uzabase, Inc. All rights reserved.