10章では集約のことについて書かれている。
ここにも書かれているが、DDDの戦術的指針の中でも、最も理解されていないのが集約らしい。
はっきり言って自分も、EvansのDDD本読んでもあまりしっくりこなかったので、ここでもう一回整理してみる。
集約とは?
- 集約は不変条件を強制するひと固まりである。
- 不変条件の強制=トランザクション整合性が保たれるということ
- 結果整合性は不変条件に該当しない
- つまり、集約とは、トランザクション整合性の境界と言い切ってよいらしい。
- 「整合性の境界」の論理的な意味は、「その内部にあるあらゆるものは、どんな操作をするにもかかわらず、特定の不変条件のルールに従う」
- 別の言い方をすると、トランザクションをコミットする時点で、集約内は整合性(不変条件)を保っている必要があるということ。
- 結果、集約は主として整合性の境界を定めるものであり、オブジェクトグラフを設計したいという理由で作るものではない。
集約の目指すものは?
- 適切に設計された集約は、業務で必要とするあらゆる変更に対して、トランザクション内での不変条件の整合性を完全に維持できる。
- 同様に、境界づけられたコンテキストを適切に設計すれば、どんな場合でも一つのトランザクション内で変更する集約を一つだけに絞り込める。
「良い集約の設計」とは?
- UIや外部システム(つまりヘキサゴナルアーキテクチャでいうところの外側)からのリクエスト(コマンド)は、一つの集約が相手をする。
- できるだけ、巨大な集約にならないようにする。
巨大な集約を避けるためには?
- 1つのトランザクションが大きい場合、それに合わせると結果的に集約が巨大になってしまう。
- この場合、集約を分けて(=トランザクションを分けて)、結果整合性(※)が保たれれば良しとならないかを考えてみる。
- 大体の場合は、整合性が確定するまで遅延が発生するものの、結果整合性を満たせばよいパターンが多いらしい。
※結果整合性とは
ある限定した期間においてだけ、モデルの一貫性が保たれていない状態があること。
例えば、コマンドの実行による状態の変化と、画面の表示が同時ではない(=システム内で整合性の遅延が発生しているということ)など。
集約のルール
IDDD本には、以下のルールが掲載されている。
1.真の不変条件を、整合性の境界内にモデリングする
上にも書いたが、集約とはトランザクション整合性の境界である。
2.小さな集約を設計する
これも上に書いた通り。巨大な集約になってしまう場合は、結果整合性を採用して集約を分割できないか検討すべし。
3.他の集約への参照には、その識別子を利用する
Evansは、「ある集約が、別の集約のルートへの参照を保持してもよい」と説明している。
しかし、これは決して、参照先の集約と参照元の集約が同じ整合性の境界に属するという意味ではない。
⇒同一トランザクション内で、参照する側と参照される側の両方を変更してはいけない(集約の前提ルール)
そのため、外部の集約への参照には、その集約(ルートのエンティティ)の一意な識別子だけを利用し、オブジェクトそのものの参照(ポインタ)を保持しないようにすべし。
IDDD本では、識別子を使用した集約の参照方法として、以下の2方法が提示されている
- 呼び出し元集約内にて、リポジトリから識別子を使用して外部の集約を取得する方法
- 「切り離されたドメインモデル」というらしい
- アプリケーションサービスにて、リポジトリから識別子を使用して外部の集約を取得する方法
筆者は後者を薦めている。(理由:集約がリポジトリに依存せずに済ませられるから)
が、ここら辺はよくわからない。「リポジトリのインターフェイスはドメインモデルなので、別に集約がリポジトリに依存してもいいんじゃない?」って思うし、そもそも後者のやり方では、集約の呼び出し時に引数として集約を渡すことになると思うが、「それって識別子じゃないじゃん」と思う。
とりあえず、集約内では、別の集約のトランザクションは触らないようにするってのは意識するようにしよう。
依存性の注入を避けるでも言及。
4.境界の外部では結果整合性を用いる
一つの集約上でコマンドを実行するときに、他の集約のコマンドも実行するようなビジネスルールが求められるのなら、その場合は結果整合性を使うこと。
CQRSでドメインモデルを構成する場合は、ドメインイベントを発行することで結果整合性を実現しやすくなる。
トランザクション整合性と結果整合性をどうやってみわける?
そもそも、トランザクション整合性と結果整合性はどちらがすぐれているのだろうか?
実はどちらが優れているというのはなく、ユースケースから、データの整合性を保つのが誰の役割(責務)なのかに注目する。
- データの整合性を保つのが「ユースケースを実行するユーザー自身」の役割ならば、トランザクション整合性を保つべし
- データの整合性を保つのが「別のユーザー(あるいはシステム)」の役割ならば、結果整合性を受け入れるべし
結果整合性はどうやって実装する?
ドメインイベントを発行し、サブスクライバ側で処理を委譲する。
集約の実装的な話
一意な識別子について
集約はルートエンティティとして、グローバルに一意な識別子を設ける必要がある。
一番単純なやり方として、UUID形式で識別子を割り振る方法がある。
パーツは値オブジェクトにする
集約のパーツを設計するときは、可能な限り、エンティティではなく値オブジェクトを使おう。
依存性の注入を避ける
依存性の注入を使ってリポジトリやドメインサービスを集約に差し込むことは、一般的に好ましくないとされている。
個人的にはこれがわからん・・・。
同じような質問がstackoverflowにあるけど、釈然と来ない・・・。(増田さんが回答されている・・・!!が、なんかちょっとズレてる?永続化のためにリポジトリを参照するわけではないと思うが、リポジトリ自身はデータを取り出すためのアクセスか永続化のためのアクセスかが区別つかないからってこと?CQRSでコマンドとクエリのリポジトリを分ければ問題ないと思うが。うーん・・・)
2018/04/25 追記
実際にコーディングしていて気づいたこと。
DI でリポジトリを集約に差し込むと、集約内でリポジトリからデータを取得できる。この際、以下の問題点があることに気づいた。
- 集約内でリポジトリからデータを取得できるが、それはグローバル変数にアクセスするようなものとなってしまう。便利ではあるが、結果として以下の問題をはらむ
- 他のトランザクションにより、リポジトリから取得できるデータが変更される場合がある。そうなると、トランザクション整合性が崩れてしまう
まとめ
ということで、IDDDの10章「集約」を備忘録としてまとめました。
結局、肝心かなめの「集約内での他集約へのアクセス方法」がしっくりきていないのがなんとも・・・