■ 1. DDDにおける集約とファットリポジトリ化の問題
- 集約の重要性:
- 関連するエンティティと値オブジェクトを1つの単位として扱う概念
- ビジネスルールの整合性を保つための境界を定義する
- データの一貫性を保証する役割を果たす
- ファットリポジトリ化の問題:
- 1つのリポジトリに多くのメソッドが集約される
- 変更影響範囲の拡大により他メソッドへの影響考慮が必要になる
- テストの複雑化が発生する
- 責務の曖昧化が生じる
- チーム開発での競合が増加する
- アクション単位設計の設計思想:
- 各アクションに対して専用のプレゼンターとユースケースとリポジトリを用意する
- 各アクションが独立し変更影響が局所化される
- DDDの層構造とADRパターンを組み合わせる
■ 2. ADRパターンの概要
- ADRパターンの定義:
- Paul M. Jonesによって提唱されたMVCパターンの代替アーキテクチャ
- HTTPリクエストの処理を3つの明確なコンポーネントに分離する
- 3つのコンポーネント:
- Action: HTTPリクエストを処理するエントリーポイント
- Domain: ビジネスロジックとドメインモデル
- Responder: レスポンスデータの構築とHTTPレスポンスの生成
- MVCパターンとの違い:
- MVCではControllerがModelとViewの両方に依存し責務が曖昧になる
- ADRではActionとDomainとResponderの役割が明確に分離される
- メリット:
- 各コンポーネントの責務が明確になる
- 各コンポーネントを独立してテストできる
- ビジネスロジックがフレームワークから独立する
- Domain層がプレゼンテーション層に依存しない
- DDDとの組み合わせ:
- ActionをPresentation層のControllerにマッピングする
- DomainをDomain層にマッピングする
- ResponderをPresentation層のPresenterにマッピングする
■ 3. 従来の設計パターンの問題点
- 集約にこだわった設計の特徴:
- 1つのリポジトリに複数の操作を集約する
- OrderRepositoryにcreateやupdateやcancelなど多数のメソッドが存在する
- 問題点の詳細:
- 変更影響範囲の拡大: 1つのメソッド変更時に他メソッドへの影響考慮が必要
- テストの複雑化: 各メソッドのテスト時に他メソッドとの相互作用を考慮する必要がある
- 責務の曖昧化: データアクセスだけでなくビジネスロジックも含まれる可能性がある
- チーム開発での競合: Gitのマージコンフリクトが発生しやすくなる
■ 4. 提案する設計パターン
- アクション単位設計の構成:
- 1プレゼンター: 各アクション専用のプレゼンター
- 1ユースケース: 各アクション専用のユースケース
- 1リポジトリ: 各アクション専用のリポジトリ
- 各アクションをパーティションで区切るイメージ
- 依存性逆転の法則:
- Application層にインターフェース定義を配置する
- Presentation層とInfrastructure層はApplication層のインターフェースに依存する
- Application層が外部層に依存しない構造を実現する
- ADRパターンとの対応:
- Action: OrderControllerとしてHTTPリクエストの受付とルーティングを担当
- Domain: Domain/Entities/Orderとしてビジネスルールとエンティティを担当
- Responder: CreateOrderPresenterとしてレスポンスデータの構築を担当
- ARC パターン:
- DDDの構造設計とADRパターンを融合させた設計
- Action-Responder Cleanパターンと呼称する
■ 5. Laravel実装例
- ディレクトリ構造:
- Application層: インターフェース定義とユースケース
- Domain層: エンティティ
- Infrastructure層: リポジトリ実装
- Presentation層: プレゼンターとリクエストとレスポンス
- Http層: コントローラー
- 各レイヤーの役割:
- リクエストDTO: リクエストデータを保持する
- レスポンスDTO: レスポンスデータを保持する
- プレゼンターインターフェース: アクション単位で独立したインターフェースを定義
- プレゼンター実装: リクエストとレスポンスの保持と取得
- リポジトリインターフェース: データアクセスの抽象化
- リポジトリ実装: Eloquentを使用したデータアクセス
- ユースケース: ビジネスロジックの実装
- コントローラー: HTTPリクエストの受付とバリデーション
- 依存性逆転の法則のメリット:
- ビジネスロジックの独立性により外部の実装詳細に依存しない
- 実装の交換が容易でインターフェースが変わらない限り影響がない
- モックやスタブを使ったテストが可能
- フレームワークからの独立性により移行時もビジネスロジックを再利用可能
- Responderの責務に関する設計選択:
- 現在のアプローチ: PresenterがCreateOrderResponseを返しコントローラーがJsonResponseを構築する
- 代替アプローチ: Presenterが直接JsonResponseを返す
- 現在のアプローチはフレームワークからの独立性を重視する
- 代替アプローチはADRパターンの徹底を重視する
■ 6. アクション単位設計のメリット
- 独立性:
- 各アクションが独立し変更影響が局所化される
- 注文作成のロジック変更時に注文キャンセルのロジックに影響を与えない
- テスタビリティ:
- モックが容易で単体テストが書きやすい
- 各コンポーネントを独立してテストできる
- 保守性:
- 機能追加や変更時の影響範囲が明確になる
- 新機能追加時に既存コードへの影響を最小限に抑える
- 可読性:
- 各クラスの責務が明確でコードの理解が容易
- 1クラスのステップ数が200から300程度に収まる
- 新しいメンバーのプロジェクト理解が速やかに進む
- スケーラビリティ:
- チーム開発での競合が減り並行開発が容易になる
- 複数の開発者が異なるアクションを並行して開発できる
- 依存性の明確化:
- 依存関係が明確で変更に強い設計になる
- インターフェースを変更しない限り実装変更が他層に影響を与えない
- ADRパターンの利点:
- ActionとDomainとResponderの分離により責務が明確になる
- ビジネスロジックがフレームワークから独立しフレームワーク変更への耐性が向上する
■ 7. 実践的なTips
- Laravelサービスコンテナ活用のベストプラクティス:
- 各機能ごとにサービスプロバイダーを作成し依存関係を明確に管理する
- インターフェースに依存することでモック化が容易になる
- プレゼンターは都度生成しリポジトリはシングルトンなど適切なライフサイクルを選択する
- テストコードの書き方:
- 依存関係が明確で必要なモックを特定しやすい
- 1つのアクションに焦点を当てたテストが書きやすい
- インターフェースに依存しているためモックの作成が簡単
- データベースや外部APIに依存せず単体テストが高速に実行できる
- ユースケースのテスト例:
- リポジトリをモック化してテストする
- 在庫不足時の例外スローを確認する
- コントローラーのテスト例:
- プレゼンターとユースケースをモック化する
- レスポンスのステータスコードとデータを確認する
- リポジトリのユニットテスト:
- DBファサードとEloquentモデルをモック化する
- データベースに依存せずテストが高速に実行される
- エッジケースのテストが容易になる
- 拡張時の注意点:
- 新しいアクションには新しいコンポーネントを作成する
- 既存コンポーネントの変更を避け影響範囲を最小化する
- 共通処理はドメインサービスやアプリケーションサービスとして抽出する
- 共通処理の抽出例:
- ドメインサービス: 価格計算や配送料計算などビジネスロジック
- アプリケーションサービス: 在庫チェックなど複数アクションで共通する処理
- パフォーマンス対策:
- リポジトリの最適化により必要なデータのみを取得する
- 頻繁にアクセスされるデータはキャッシュを実装する
- Eloquentのwithメソッドを活用しN+1問題を回避する
- ADRパターンとDDDの組み合わせガイドライン:
- Action層の薄さを保ちビジネスロジックを含めない
- Domain層の純粋性を保ち外部層に依存しない
- Responder層の独立性を保ちビジネスロジックを含めない
■ 8. まとめ
- 設計の重要性:
- 集約にこだわりすぎることでファットリポジトリ化し保守性が低下する
- アクション単位設計により各アクションが独立し変更影響が局所化される
- DDDとADRパターンの組み合わせによりクリーンアーキテクチャの原則を実現する
- 依存性逆転の法則により外部層がApplication層に依存する構造を実現する
- 長期的なサービスへの適用:
- 数年から数十年と長く運用し続けるサービスにこそ価値がある
- 機能の追加や変更が頻繁に発生する環境に適している
- チームメンバーの入れ替わりに対応しやすい
- 技術スタックの進化に柔軟に対応できる
- 複数の開発者の並行作業で競合が発生しにくい
- 長期運用における優位性:
- 機能追加時の影響範囲が明確で既存コードへの影響が最小限
- コードの理解が容易で新メンバーも特定機能に焦点を当てて理解できる
- フレームワークからの独立性により技術スタック変更に柔軟に対応できる
- 並行開発の促進により複数開発者が同時開発しても競合が発生しにくい
- 実践的な設計指針:
- アクション単位設計に従い各アクションに専用コンポーネントを作成する
- 依存性逆転の法則に従いApplication層にインターフェースを配置する
- ADRパターンとDDDの層構造を組み合わせる
- テスト容易性と保守性を重視する