/note/tech

Always-Valid Domain Model

要約:

■ 1. 記事の背景と目的

  • 新しいコース制作: Pluralsightでバリデーションとドメイン駆動設計(DDD)に関する新しいコースを制作中
  • 使用ライブラリ: FluentValidationライブラリと.NETデータアノテーション(属性)を使用
  • 元記事への応答: Jeffrey Palermo氏の11年前の記事とGreg Young氏のそれへの応答を再読し、新たな応答を執筆

■ 2. 常に有効でないドメインモデルの主張

  • Jeffrey Palermo氏の主張: 常に自身を無効な状態から守るドメインエンティティを作成するのは非実用的
  • 理由:
    • バリデーションルールはコンテキスト依存で、すべてのユースケースに平等に適用できない
    • バリデーション中に返されるメッセージはプレゼンテーション層の責任であり、ドメイン層に置くべきでない
    • 既存のバリデーションルールは過去のデータに適用できない可能性がある

■ 3. コンテキスト依存のバリデーションルール

  • Greg Young氏の応答: 各ドメインクラスには不変条件(invariants)が付随している
  • 不変条件の定義: 常に真でなければならない条件で、それがドメインクラスを定義する
  • 例: 三角形は3つの辺を持つ概念で、edges.Count == 3は三角形の本質的条件。4つ目の辺を追加すると四角形になり、三角形ではなくなる
  • DRY原則への貢献: ドメインクラス自身がチェックを担当することで、複数の操作で同じチェックを漏らさないようにする精神的負担を軽減

■ 4. バリデーションと不変条件の違い

  • 一般的な認識: 入力バリデーションと不変条件は異なる概念と見なされることが多い
  • 実際: 両者はまったく同じもの。アプリケーションの不変条件がなければ、入力データをバリデーションする必要はない
  • 例: メールアドレスの妥当性チェックは、問題ドメインがすべてのメールアドレスを特定のルールに従わせることを要求するから存在する
  • 視点の違い: 入力バリデーションと不変条件の違いは視点の問題に過ぎない

■ 5. ヘキサゴナルアーキテクチャの視点

  • ドメイン層の視点: ビジネスルールを不変条件として扱う
  • アプリケーション層の視点: ビジネスルールを入力バリデーションルールとして扱う
  • 違反の扱い方:
    • ドメインモデルでの不変条件違反: 例外的状況であり、例外をスローすべき
    • 外部入力の不正: 例外的ではないため、例外をスローせずResultクラスを使用すべき

■ 6. 二重防御の実装例

  • 第一防御線: 外部アプリケーションからの無効な入力をフィルタリングし、ドメインモデルに渡す前に除外する
  • 第二防御線: データがドメインモデルの境界に入ったら有効であると仮定し、この仮定に違反する場合はバグとして早期に失敗する
  • コード例:
    • コントローラ: 学生名をチェックし、不正な場合はエラーメッセージを返す(フィルタリング)
    • ドメイン層: 同じチェックを行うが、これは不変条件チェックであり、例外をスローする(最後の防御線)

■ 7. シンプルなバリデーションと複雑なバリデーションの誤解

  • 一般的な区別: シンプルなバリデーションを「データバリデーション」、複雑なバリデーションを「ビジネスルールバリデーション」と呼ぶことがある
  • 誤解: この区別は不正確。データバリデーションとビジネスルールバリデーションは同じもの
  • 理由: この区別は、入力バリデーションを扱う2つの方法を正当化するために行われる
  • 実態: すべてのバリデーションは、その単純さに関わらず、ビジネス要件とその結果であり、単に複雑さが異なるだけ

■ 8. コンテキストはビジネスルールの一部

  • バリデーションルールの文脈依存性: 多くのバリデーションは呼び出されるコンテキストに依存する
  • 無効な状態の回避: これはエンティティがバリデーション前に無効な状態に入る必要があることを意味しない
  • コンテキストの組み込み: コンテキスト自体がビジネスルールの一部
  • 大規模コンテキスト: 境界づけられたコンテキスト(Bounded Context)パターンで分離
  • 小規模コンテキスト: ドメインクラス自体にエンコードすべき

■ 9. CanExecuteパターンの実装

  • 例: アクティブな学生のみが新しいコースに登録できる
  • 実装方法:
    • CanEnrollInメソッド: アプリケーション層が入力バリデーションに使用するAPIを提供
    • EnrollInメソッド: 同じAPIを使用して不変条件をチェックし、登録が不可能な場合は例外をスロー
  • 効果: ドメイン知識が常にドメイン層に残る

■ 10. 型システムへの依存

  • ガード句の代替: 型システムに依存する方がさらに良い方法
  • 例: 学生と講師を区別する
  • 悪い実装: Personクラスにタイプ列挙型を持たせ、ガード句で操作を制限
  • 良い実装: StudentクラスとInstructorクラスを個別に作成し、各自の操作のみを担当
  • 利点: コンパイル時にエラーを検出でき、フィードバックループが短くなる(Fail Fastの原則を極限まで適用)

■ 11. データ契約とドメインモデルの分離

  • 問題の根源: ドメインモデルを無効な状態に入れることを可能にする考え方は、データ契約(DTO)とドメインクラスの混同から生じる
  • 解決策: ドメインモデルとアプリケーションデータ契約を分離し、独立して変化できるようにする
  • 追加の利点: クライアントアプリケーションとの後方互換性を壊すことなくドメインモデルをリファクタリングできる

■ 12. エラーメッセージの責任

  • Jeffrey氏の指摘: バリデーション中に返されるメッセージはプレゼンテーション層の責任
  • 問題: ドメインクラスがエラーテキストメッセージを担当することになる
  • 解決策:
    • 個別のErrorクラスを導入し、アプリケーションで可能なすべてのエラーを列挙
    • プレゼンテーション層でErrorインスタンスをテキストメッセージに変換
    • ドメイン層ではエラーコードを使用し、テキストメッセージを削除

■ 13. 過去のデータのバリデーション

  • 問題: 既存のバリデーションルールが過去のデータに適用されない場合
  • 例: アプリケーションが学生に住所入力を要求しなかったが、現在は必須になった場合
  • 対処オプション:
    • 新しいルールに準拠しない既存データをすべて削除(本番環境では不可能)
    • Studentクラスに古いルールと新しいルールの両方を処理させる
    • 新しいルールセット用に個別のStudentクラスを作成(推奨)

■ 14. バリデーション強化の実装

  • バリデーション強化: 古いデータが新しいビジネスルールに準拠しない状況
  • ビジネス判断: 古いデータをどう扱うかはビジネスの決定事項
  • 実装方法: 古いデータと新しいデータを区別するために個別のクラスを導入し、移行期間を作成
  • 例: StudentWithAddressサブクラスを作成し、すべての学生が住所を入力したら古いStudentを削除してStudentWithAddressをStudentに改名
  • 明示的なモデリング: この移行期間とビジネス判断をドメインモデルに明示的に反映すべき

■ 15. 大きなUIフォームの処理

  • 問題: 10、20以上のフィールドを含む大きなUIフォームで、ユーザーが途中で保存して後で続けたい場合
  • 誤った解釈: フォームに関連するエンティティが無効な状態に留まる必要があるのではないか
  • 正しいアプローチ:
    • 進捗を保存する機能は別のビジネス判断
    • StudentApplicationなど別のドメインエンティティを作成し、より緩い不変条件のセットを持たせる
    • ユーザーがフォームを完成させて「登録」をクリックしたら、すべてのバリデーションを行いStudentApplicationをStudentに変換
  • 現代的アプローチ: 大きなフォームを複数のシンプルなフォーム(ウィザード)に分割するのが一般的だが、これも中間エンティティの必要性を強調

■ 16. まとめ

  • ドメインクラスの責任: 常に無効な状態になることから自身を守るべき
  • 不変条件の役割: ドメインクラスを定義するものであり、逆ではない
  • バリデーションと不変条件の関係: 同じビジネスルールから生じ、視点の違いに過ぎない
  • 例外処理の使い分け:
    • 不変条件違反: 例外的状況なので例外を使用
    • 無効な入力: 例外的ではないのでResultクラスを使用
  • バリデーションの統一性: 「データバリデーション」と「ビジネスルールバリデーション」に違いはなく、すべてビジネス要件の結果
  • コンテキストの扱い: バリデーションコンテキストはドメイン知識の一部であり、ドメインモデルに置くべき
  • メッセージの処理: テキストメッセージをドメイン層から削除するには、エラーコードを使用してプレゼンテーション層でテキストに変換
  • バリデーション強化: 新しいルールセットを扱う個別のクラスを作成し、すべての既存データが新しいルールに準拠したら古いクラスを削除
  • 大きなフォームの処理: 保存前に提出する必要がある大きなUIフォームには、より緩いバリデーションルールを持つ個別のエンティティを作成