会社で「ドメイン駆動設計 モデリング/実装ガイド」の読書会に参加しましたが、
内容を忘れかけていたのでアウトプットして整理します。
前半はこちら。
第6章 ドメイン層の実装
ドメイン層に所属する要素について説明。
エンティティ
(概念はなんとなく分かったのですが、言葉にしづらい...)
エンティティは、一意なモノを表すオブジェクトのこと。
例えば、社員IDと、名前、身長、体重、所持金などを属性として持つ「社員クラス」があるとする。
このlクラスからなる社員オブジェクトは、たとえ身長、体重、所持金が同じ「山田さん」が二人存在するとしても、「社員ID」があるため、別人と認識されている。
このように、一意なモノを表現するオブジェクトのことをエンティティという。
値オブジェクト
エンティティとは逆に、一意に識別して管理する必要がないオブジェクトのこと。
注意点は、値オブジェクトは使い捨てということ。
値オブジェクト内の属性は生成されたときに決まっており、変更されない。
エンティティと値オブジェクトについて
(自分の考え整理のためにもう一例考える。)
エンティティの例
同じこうげき、ぼうぎょ、HPを持った同じ図鑑No244のポケ◯ンが2匹いたとしても、
内部的には、2匹それぞれに固有のIDがあって、2匹は区別されている。
そのため、片方にタウリンを使ったら、ちゃんと片方のこうげきのみが上がる。
値オブジェクトの例
とある図鑑No133のポケ◯ンがNo134のポケモンに進化した場合、そのポケ◯ンと紐づいている種族オブジェクトの中身を詰め替えてNo133のポケ◯ンからNo134のポケ◯ンに変更したりはせず、新しくNo134のポケ◯ンのオブジェクトを作成しなおす。
ドメインサービス
できるだけ使うことを避けるもの。
極力エンティティと値オブジェクトで実装しなければ、ドメインサービスがビジネスロジックのように膨らんでしまう恐れがある。
モデルをオブジェクトとして表現すると無理があるものの実装に使うサービス。
たとえば、同じメールアドレスを持ったユーザがいるかを確認するロジックは、各ユーザオブジェクトが他ユーザの情報を持っていないため、ドメインサービスで実装するしかない。
リポジトリ
永続化層へのアクセスを(集約単位で)提供するもの。
- リポジトリは集約ごとに1つ存在
- リポジトリから返されるものは必ず「集約ルート」のオブジェクト(エンティティ)
- 子オブジェクトは集約ルートオブジェクトがインスタンス参照した状態で扱う
- 子オブジェクト用のリポジトリなどは作らない
オニオンアーキテクチャでの実装
オニオンアーキテクチャでは、リポジトリのインターフェースがドメイン層、実装がインフラ層になる。
これにより、ドメイン層は、実際にDBへどう格納するかなどの知識を一切持たなくなるため、ドメイン知識にのみ集中することができる。
ファクトリー(ドメインサービスの一種)
オブジェクトの生成ロジックが複雑な場合や、他の集約を参照しないと生成できない場合、**オブジェクトの生成の責務のみを持ったファクトリーオブジェクトを作成する**。
ファクトリーは、ドメイン地シクの表現のために、リポジトリを参照できる。
それ以外のオブジェクト
ドメイン層には、上記以外のオブジェクトを作成することも可能。
ただし、以下の概念には従うこと。
- 高凝縮・低結合にする
- ドメイン知識を表現する
- モデルを直接表現するオブジェクトを書く
複数の集約間での整合性確保
方法は以下の通り。
- ユースケース層のメソッドで整合性を確保する
- ドメイン知識を持たないユースケース層に整合性を任せることになるため、間違った使い方をされる可能性がある
- ドメインイベントを用いて結果整合性を確保する
- ドメイン層オブジェクトの操作に対してイベントを発火させ、別集約の操作を呼び出す
- 技術的にむずかしい
まずは用意に実装できるユースケース層のメソッドで整合性を確保する方法を試すべき。
第7章 ユースケース(アプリケーション)層の実装
ユースケースとは
ドメイン層のクラスが後悔しているメソッドを組み合わせて、ユースケースを実現する部分。
リポジトリへの永続化依頼も行う。
あくまでドメイン層の機能を使って「何をする(What)」を表現することがユースケース層の責務。
「どうやって(How)」はドメイン層に隠蔽されているため、これが実現できる。
ユースケースからの戻り値
ユースケースから、プレゼンテーション層に値を返す方法は以下の2通り。
- 専用の戻り値クラスに詰めて返す
- メリット:ドメインオブジェクトに、プレゼンテーション層の処理が入ることを防げる
- メリット:ドメイン層に修正が発生しても、影響を受けない
- デメリット:詰め替えがめんどう
- ドメインオブジェクトをそのまま返す
- メリット:データの詰め替えが発生しない。楽。
- デメリット:油断するとドメイン層にプレゼンテーション層の処理が入ってしまう
- デメリット:ドメイン層の修正によって影響をうける
専用の戻り値クラスに詰めて返す場合、クラス名についての定義はないため、DTOなど自由に決める。
ただし、ユースケース層→プレゼンテーション層以外でのデータ受け渡しオブジェクトと区別できるようにすべき。
第8章 CQRS (ユースケース層)
CQRS:コマンドクエリ責務分離(Command and Query Responsibility Segregation)
「参照に使用するモデル」と「更新に使用するモデル」を分離すること。
DDDの参照系処理で発生する問題
DBなどからのデータ取得はリポジトリを利用するが、一覧画面表示のような処理を行う場合に、複数の集約の情報を一気に取得する必要があり、以下の例のような不都合が生じる。
- 複数の集約から値を取得し、プレゼンテーション層に返すように加工する処理が多重ループになってややこしい
- 画面表示に必要ない値まで取得するため負荷がかかる
- 複数集約の条件による絞り込みなどができない
解決策
「参照に使用するモデル」と「更新に使用するモデル」を分離する。
- 更新系モデル
- ドメインオブジェクトを使用
- 参照モデル
- 特定のユースケースに特化したオブジェクトを作成する
- サービスも参照モデル用に定義する
参照モデルを定義するレイヤー
- ユースケース層 (抽象的な知識(What)のみを保持する)
- クエリサービスのインターフェィス
- 戻り値の型(DTO)
- インフラ層 (具体的な値の取得方法(How)のみ保持する)
- クエリサービスの実装クラス
DBなどからのデータ取得方法(How)をインフラ層に隠蔽することで、クエリを組み換えてパフォーマンスのチューニングがやりやすくなる。
デメリット
- ドメインオブジェクトのデータ参照箇所が追いづらくなる
- アーキテクチャ自体が複雑になる
デメリットもあるため、常に使用すればいいものではない。
実装時の注意事項
部分的導入の可否
必要なところだけ、参照とに特化したモデルを導入するのもアリ。
型を定義するレイヤーがユースケース層である理由
ユースケースごとに最適な参照モデルが必要だから。
どんな参照モデルが必要かは各ユースケースと密接に紐づいているため、その方が都合がいい。使い回す方がややこしくなる。
更新系との整合性を確保する方法
ユースケース層でテストを書く。
よくある誤解
データソース分離の必要性
CQRS = データソース分離(参照用DBと更新用DBを分けるなど) ではない。
イベントソーシングとの関係
CQRS = イベントソーシング ではない。
第9章 プレゼンテーション層の実装
プレゼンテーション層の処理概要
各クライアントから、そのクライアントの種類(ブラウザ、スマホアプリ、APIなど)専用のプレゼンテーション層のコントローラがリクエストを受け取り、レスポンスを返す。
##プレゼンテーション層のクラス、ファイル
以下のようなクライアントとの入出力関連クラスやファイル。
- コントローラ
- HTMLテンプレート
- (フレームワークの)ルーティング設定ファイル
リクエスト仕様の定義
リクエスト仕様の定義は以下のようなもの。(HTMLレンダリングを行う場合は暗黙的に決まる)
- 通信プロトコル
- エンドポイント
- パラメータ
レスポンス仕様の定義
レスポンス仕様の定義は以下のようなもの
- UI関連
- 書式(1,000のように3桁でカンマをつけるなど)
第10章 アーキテクチャ全般・ライブラリなど
例外処理
例外の種類
- 想定外の例外(500系エラーなど)
- 内部エラーとして共通のレスポンスを返す
- 非検査例外を投げ、プレゼンテーション層の共通クラスでキャッチ → クライアントに合わせてレスポンスを返す
- ユースケースの中で発生しうる想定された例外(400系エラーなど)
- 内容によって、エラーを投げるレイヤーが変わる (バリデーションによるエラーのため)
バリデーション内容の重複
ドメイン層とプレゼンテーション層で同じバリデーションを入れたくなる時がある
- プレゼンテーション層:API仕様書に書いてあるなどの理由でバリデーションが責務になった場合
- ドメイン層:ドメインの制約を守るためにバリデーションが必要になった時
デメリットとして、仕様変更時に修正漏れになる可能性があるので、考えること。
処理結果を例外で表現しない場合
例外でなく、処理結果を記載したオブジェクトを返す。
- メリット
- 例外を使用することが一般的でない言語やフレームワークで使いやすい
- デメリット
- 分岐が増える (チェックのたびに分岐してreturnが発生する)
パッケージ
レイヤーをパッケージのルート階層で分ける。
Webフレームワークへの依存
- ドメイン層:一切依存しないことが望ましい (DIライブラリは除く)
- ユースケース層:部分的な依存は必要
- トランザクションは必要
- ログ出力はインターフェースのみ設置し、インフラ層で実装すれば、依存せずに済む
- プレゼンテーション層:依存は必要
- リクエストの受け取り
- ルーティング
- フレームワーク固有の実装はここに封じ込める
ORマッパー
ORマッパーのクラスをそのままドメイン層のクラスとして利用してはいけない
- 全ての項目にsetterがある → 制約がかけられない
- テーブルとオブジェクトが切り離せない → オブジェクトとテーブルの設計に望ましくない制約がかかる
言語
DDDを行うときは、静的型付けの言語が良い。
動的型付け言語は向いていない。(制約がかけにくいため)
(タイプヒントと静的解析の組み合わせができる言語ならできないこともない。)