メインコンテンツまでスキップ

テストから書くとドメインモデルはどう変わるか

テストを先に書くと、モデルの形が変わることがあります。この章はその範囲を扱います。

先に断っておくと、この章は「TDD を使えば良いドメインモデルができる」とは主張しません。テスト先行が何を見えるようにし、何を見えるようにしないかを書きます。後者の方が実務では重要です。

DDD の戦術パターンそのものは扱いません。値オブジェクトをどう設計するか、集約の境界をどう引くかは、それぞれ専用の章があります。この章に出てくるコードはテストだけです。

この章で学ぶこと

  • テストを先に書くと、モデルの何が見えるようになるか
  • 見えたものを設計の根拠として扱うときの限度
  • テストからは決まらないもの
前提知識

03 テストが設計を圧す07 境界を差し替える を使います。DDD の用語を知らなくても読めますが、詳しく学ぶなら下の参照先へ進んでください。

この章で扱わないこと

この章は観察だけを書きます。設計そのものは、すべて既存のガイドが扱います。

観点参照先
値オブジェクトの設計 (不変性・等価性・自己検証)値オブジェクト
エンティティと識別子エンティティ
集約の境界と整合性集約
ドメインと永続化の境界リポジトリパターン
ドメインイベントドメインイベント
ユビキタス言語、境界づけられたコンテキスト戦略的設計
業務からモデルを見つける手法イベントストーミング
原則とパターンの対応OOP と DDD の戦術パターン
層ごとのテスト戦略、モックの配分テスト戦略

1. 依存がコンストラクタへ出てくる

07 章で見たとおりです。テストから対象を組み立てようとすると、内部で作っていたものを外から渡す形になります。

// テストが要求した形
$useCase = new ReserveRoom($clock, $repository, $notifier);

結果として、そのクラスが外の何に触るかがコンストラクタに並びます。

これは DDD の教義から導いたのではありません。テストを書くという制約が押し出しただけです。同じ形は、DDD と無関係なコードでも出ます。

ただし帰結は近いところに着地します。ドメインの中心にあるオブジェクトが、データベースや時計を直接呼ばない形になるからです。

2. 等価性を問われる

テストを書こうとすると、「これとこれが等しい」と書く場面が来ます。

test('同じ時間帯を表す 2 つは等しい', function () {
$a = new TimeRange(at('10:00'), at('11:00'));
$b = new TimeRange(at('10:00'), at('11:00'));

expect($a->equals($b))->toBeTrue();
});

equals() はまだありません。テストを書こうとして初めて、必要だと分かります。

このテストを書くには、何が等しければ等しいのかを決める必要があります。開始と終了が同じなら等しいのか。それとも、別々に作った予約は別のものなのか。

決めた結果が、そのオブジェクトの性質を決めます。値として扱うのか、同一性を持つものとして扱うのか。DDD ではこれを値オブジェクトとエンティティとして区別しますが、テストは区別を教えてくれません。問いを出すだけです。

答えるのは業務の側です。「同じ時間帯の 2 つの予約は同じ予約か」は、コードを見ても分かりません。

3. setup の痛みが境界を疑わせる

1 つのテストを書くのに、5 つも 6 つもオブジェクトを組み立てることがあります。次は 06 章からの続きではなく、組織や建物まで抱えた別の設計を仮に置いた例です。

test('予約を受け付ける', function () {
$room = new Room(new RoomId('room-1'), '会議室 A', 8);
$building = new Building(new BuildingId('b-1'), '本社', [$room]);
$organization = new Organization(new OrgId('o-1'), [$building]);
$policy = new ReservationPolicy($organization, maxHours: 4);
$calendar = new Calendar($organization, holidays: []);
// ここまで来てようやく本題
});

これは何かを言っています。ただし言っている内容は 1 つではありません。03 章の型 4 で触れたとおり、可能性は 2 つです。

  • 対象が持ちすぎている。1 つの操作に必要な協力者が多すぎる
  • テストの書き方が冗長なだけで、対象の設計には問題がない

見分ける手がかりは、組み立てたもののうち、そのテストの本題に関係するものがいくつあるかです。上の例で「予約を受け付ける」の本題に関わるのは、部屋と、上限時間くらいです。建物や組織は、部屋にたどり着くために作っているだけです。

この場合、対象が広い範囲を一度に要求していることになります。整合性を保つ単位を狭くできないかを疑う材料になります。

ここで止めます。「setup が長い、だから集約の境界が間違っている」とは言えません。テストデータビルダーのような道具で短くできるなら、それで済む話かもしれません。テストが出すのは疑う材料であって、結論ではありません。

4. テストからは決まらないもの

ここまでの 3 つは、テストが何かを見せてくれる話でした。同じ重みで、見せてくれないものを挙げます。

用語。Reservation と呼ぶか Booking と呼ぶか。この判断にテストは関与しません。業務の現場で使われている言葉を使うべきで、その言葉は現場でしか分かりません。

**境界の位置。**予約と請求を同じシステムで扱うのか、分けるのか。これも業務の側の判断です。テストはどちらの構成でも書けます。

**何を作るか。**そもそも会議室予約に何の機能が要るのか。テストは「作ると決めたもの」が正しく動くかを確かめますが、作るべきかどうかは扱いません

**モデルの妥当性。**テストが全部緑でも、モデルが業務の実態からずれていることはあります。テストは自分で書いた期待に対して緑になるだけで、その期待が業務と合っているかは別の話です。

この 4 つは、DDD で言えば戦略的設計にあたる領域です。**テスト先行はここには届きません。**この章が「DDD は 1 章だけ」で足りると判断した理由がこれです。テストから見えるのは戦術の側だけで、それも問いの形でしか出てきません。

相性について

「TDD と DDD は相性が良い」という言い方があります。この章の観察から言えるのは、次の範囲です。

**言えること。**テスト先行で書くと、依存が外に出て、等価性を問われ、setup の重さが測れます。これらは DDD が扱う判断と重なる領域です。

**言えないこと。**TDD を使えば良いドメインモデルができる、とは言えません。テストが出すのは問いであって、答えは業務の側にあります。答えを間違えたまま緑にすることは容易です。

TDD を設計の手段として位置づける立場は、Freeman と Pryce の仕事に整理があります1。ドメインモデルを業務から見つける手法とは別の系統の話なので、両方を読むと補い合います。

まとめ

  • テスト先行は、依存をコンストラクタへ押し出します。DDD の教義からではなく、テストの制約からそうなります
  • 等価性の判断を問われますが、答えは業務の側にあります。テストは問いを出すだけです
  • setup の重さは境界を疑う材料になります。材料であって結論ではありません
  • 用語、境界の位置、何を作るか、モデルの妥当性は、テストからは決まりません
  • 「相性が良い」と言えるのは戦術の側に限られ、それも問いの形で重なるという範囲です

次に読む

Footnotes

  1. TDD を設計を導く手段として扱う立場は、Steve Freeman と Nat Pryce の『Growing Object-Oriented Software, Guided by Tests』(2009) に整理があります。なお Eric Evans の『Domain-Driven Design』(2003) は TDD をほとんど論じていないので、「テストがモデルを形作る」の典拠にはなりません。