単体テストとMVCとDDDに対する個人的見解

NOTE: 2014-01-08 追記あり。

前回のエントリでレガシーな開発環境を改善している話をした。

レガシーな開発環境の上にはレガシーなプロジェクトが存在していて、これを改善するためにどうすれば良いかを考える課程で、MVCとDDDに対する考えがまとまってきたのでアウトプットしておく。(まとまってきただけで実務レベルまではまだ落とし込めていない感)

自分が担当しているとあるWebコンテンツの話。 本題ではないので簡単に前置く。

  • PHP
  • 仕様書、ドキュメント無し
  • テスト無し
  • 独自カスタマイズされたWAF
  • コピペ、使用されていないソース
  • 使用されていないテーブル、カラムの点在
  • Fat Controller

そんな状態なわけで、当然のように日々の改修や新機能の追加でエンバグを起こす。しかも大人の事情で開発メンバーは突然入れ替わったりする。 これまでは、気をつけようとかしっかりチェックしていこうといった話で根本的な改善を行わなかった。(行えなかったのかもしれない)

しかし流石につらい。精神的にも肉体的にもつらい。 何よりエンジニア的な進歩がないのがつらい。

そこでまず何から取り組めばよいかという話になる。 ユーザーの手元に届くものであるから、バグを減らしていくことが最優先であるが、どうすればよいのか。 そんな考えからやはりビジネスロジックでのエンバグ、いわゆるイージーミスは少なくても無くしたい、単体テスト欲しいという考えに至った。

テストしやすいソースとは

そこでテストしやすいソースとはどんなものかを真剣に考えるようになった。 また、WebアプリケーションにおけるMVCとは一体どんな実装であるべきなのか。 テストしやすいMVCアプリケーションとはどのようなものなのか。

よく一つの関数に複数の役割を持たせるべきでは無いという話を聞く。 確かにそうだと思うし、テストもしやすそうではあるのだけれど、MVCに当てはめてみると割と難しくなる。

php mvc best practice」でググるCakePHPとかYiiとかの話を見つけることが出来るのだけれど、まぁビジネスロジックはモデルに突っ込め的なことが書いてある。

そうすると、コントローラークラスは確かにシンプルになりそうなんだけどモデルクラスがFatになるしモデルはテーブルと1対1のケースが多くトランザクション処理が非常に書き辛いので、トランザクションの開始と終了はコントローラクラス側で行うみたいな感じになって妙に気持ちが悪い。 何より、結局テストがしづらいことに変わりがなく、テスト用のデータベースとかダミーデータとかを準備する必要があって本当に敷居が高い。

そんなわけで、個人的にテストがしやすいソースは以下のようなものである。

  • ロジックは状態を持たなくてもよい(Fatなものでも状態を持たなければ別に問題ない。状態とはif文とかswitch文とか)
  • テストデータをデータベースに準備しなくてもよい

MVCとDDD

そしてこの考え方で色々調べていると、割とDDD(ドメイン駆動設計)の話に出てくる責務駆動設計に近い考え方だと思うようになった。というか責務駆動設計なんだろうと思う。

そうすると、MVCは以下のような感じになった。

  • M -> E(Entity)
  • V -> View
  • C -> Controller

上記にServiceやStructureといったロジック層を加えるとうまくいく気がしている。

Entityとは文字通り実態を表し、現在モデルと呼ばれているクラスはデータ取得のみを行う。ここはWAFの機能を利用してなるべくテストをする必要がないようにしたい。

コントローラはドメインを表し、そのドメインで行うべき制御を行うという解釈。 制御とは、Entityからデータを取得し、Service、Structureに対してEntityを渡し、出力をViewへ渡すといった感じ。ServiceとStructureの違いは、Serviceはドメインに依存する処理、Structureはドメインに依存しない処理、という個人的な感覚。

こうすると、Entityは言わばWAFのモデルそのまま、あるいはSQLのみで構成されたほとんどテストする必要の無いクラスとなる。 ほとんどのWAFのモデルは各言語のクラスオブジェクトとしてEntity(DBの値)を返してくれるため、ServiceやStructureはテスト時にテストクラスでEntityを偽造することで、DBにテストデータを用意する必要もない。 そしてコントローラクラスはテストの必要のないEntity、テストされたService、Structureを扱うクラスとなるためE2Eテストを通すことが可能になりそう。(というのも現案件はE2Eテストを非常に行いづらい理由があって行えていない)

ただドメインの設計がしっかりしていないと、Serviceの部分が非常に書きづらくなるので、新機能実装時には以下の順番で行うようにした。

  • ドメイン(URL)の設計
  • ドメインに合わせたDB設計
  • Entity(モデル)クラスの実装
  • Service(及びStructure)クラス及びテストコードの実装
  • Routing(コントローラ)への組み込み
  • (E2Eテスト)
  • 動作確認

いくつかの機能を上記で実装してみたところ個人的には割とうまくいった感じがある。 ちょっとまだ日々の業務に落とし込むほどスムーズにはいかないのでまぁ模索しながらやってくしかない。

そしてこれは新機能実装時においてのみ使える話となっており、既存のレガシーコンテンツに対して手を入れる必要がある場合はまた別の手法を取らないといけない。これについては答えがない。

今のところこんな感じである。

追記2014-01-07

@ktz_aliasさんとのtwitterのやりとり。 こちらから特攻する形でもリプライして頂き感謝しています。まだまだ勉強しないといけないですね。

追記2014-01-08

@j5ik2oさんとのtwitterのやりとり。 昨日今日で、自身のDDDに対する知見の浅さとアプリケーションフレームワークに振り回されている様に気づけた瞬間でした。感謝です。