クラス設計とTDD
自動販売機
クラスがひととおり完成しました。その全体は、クラス図で示すと次のようになりました。
冒頭で示した「飲み物自動販売機の概略」図が、ほぼそのままのイメージでモデル化できています。これはTDDBC大阪のお題が優れていたからであって、いつもこのように上手く行くとは限りません。それでも、最初におおまかなクラス設計を考えておくことは大切です。
「TDDしていくなかでクラス構造が自然と作られていく」というようなことが言われますが、まったくのゼロから始めたのでは、気が遠くなるくらいの試行錯誤が必要になってしまうでしょう。実際の開発では、「TDDによってクラス設計を煮詰めていく」のが良いでしょう。事前にある程度までクラス設計を行なっておき、TDDしながら細部を詰めていく、また、クラス設計を手直ししていく、そういう感じです。
TDDしながらクラス設計を詰めていくと、テストしやすい(テスタビリティの高い)クラス設計になるであろうとは予想できます。すなわち、クラスやメソッドの粒度は小さく、メソッド内の分岐の数は少なく、クラス間は疎結合になっていきます。参考までに、コードメトリックスの結果も載せておきます。
なお、このコードメトリックス分析と前出のクラス図は、Visual Studio 2012 RCで作成しました。
TDDから見るクラス設計指針
TDDしているときに手が止まったら、それはクラス設計を改善すべきサインなのかもしれません。最後に、これまで筆者が見てきた例をいくつか挙げておきます。
-
テストが書きにくい
- 公開されているインターフェースからテスト対象が遠くてテストしにくいのかもしれません。その場合は、テスト対象をprivateからinternalに変えてしまいましょう。たとえば商品ラックが持っているビンのテストをするのに、自動販売機のインターフェースしか使えなかったとしたら、面倒なだけでなく、自動販売機にテストのためのインターフェースが増えることになってしまいます(しかも自動販売機としては本来は不要なインターフェースです)。
- 他クラスへの依存性が高すぎるのかもしれません。疎結合なクラス構成にできないか、検討してみましょう。テスト対象のメソッド(またはその一部分)を、依存先のクラスや新設のクラスに移すことも検討しましょう。
- 依存先のクラスがテストコードから制御しにくい場合は、インターフェースを定義してモックなどを使えるようにすべきかもしれません。
-
コードが書きにくい
- テストは書けたけれどコードが書きにくいのは、テストが大きすぎるのかもしれません。テストの粒度を小さくする際に、クラスも分割した方が良いことに気付くことがあります。今回、「購入可能な商品リスト」は、商品ラックの在庫リストと、コインメックの預り金を別々にテストファーストしてから、最後にまとめ上げたメソッドを作りました。これらが全部同じクラスに入っていて、最後に作ったテストケースから始めたとしたら、どうなっていたでしょう。
-
テストの準備が長い
- テスト対象のメソッドを呼び出す前の準備にたくさんのコードが必要な場合、オブジェクトの組み立てが上手く自動化できていないのかもしれません。適切な場所で組み立てていますか?自動販売機の例では、ビンを商品ラックに組み込むのは商品ラックの中でやっていますが、これを自動販売機クラスでやってしまうと、商品ラックをテストするだけのために自動販売機全体を組み立てる必要が出てきてしまいます。
- テストデータの準備などで行数が掛かっているのであれば、メソッドに括り出します。
-
テストの結果判定が長い
- ひとつのメソッドでたくさんの状態に変化が生じるのは、それが予測しにくい場合は使いにくいメソッドになります。それはクラス設計に起因しているかもしれません。
- 結果を判定するために別のオブジェクトの状態を見ないといけないのであれば、そのメソッドの場所を考え直してみるとよいかもしれません。
- オブジェクトの比較のために複数のプロパティをアサートしているのなら、オブジェクトを比較するメソッドを作った方が良いかもしれません。
- 状態を判定するために複数のプロパティをアサートしている場合、じつはそれらは状態に依存しているだけの変数で、本当に知りたい状態が隠れているのかもしれません。
-
テストクラスが肥大化してきた
- テスト対象のクラスごとにテストクラスを1つ作るという一般的なやり方の場合、これは製品クラスを分割すべきサインです。
-
アサート文が上手く書けない、不細工になる
- 本当に必要としている情報を表現できていないのかもしれません。あるいは、ジュースクラスでEqualsメソッドのオーバーライドを作ったように、比較のためのメソッドが必要なのかもしれません。
まとめ
- TDDBD大阪2.0の課題をC#でやってみた。クラス設計も同時に進めていくことになるという、良い課題であった。
- TDDで自動的にクラス設計ができたりはしない(少なくとも現実的な時間では)。クラス設計を洗練し、詳細を詰めていく補助として、TDDは役立つ。また、TDDは、小粒度で疎結合、かつテスタビリティの高いクラス設計に向かわせるバイアスを与える。