DDDにおける集約
DDDにおける集約(Aggregates)とは、オブジェクトのまとまりを表し、整合性を保ちながらデータを更新する単位となります。通常はオブジェクトの集まりの「境界線」の意味で使われ、オブジェクト群の生成/読み込み/変更/保存といったライフサイクル管理が行われます。
外部から集約を操作できる「集約ルート」
外部から集約を操作する場合、代表オブジェクトである「集約ルート(≒ルートエンティティ)」のみ参照することができます。集約ルートを操作することで集約全体の整合性を保ちながらデータを変更できます。
上図の例では、注文に関わるオブジェクト群が集約の「境界線」となっています。操作をしたい場合「注文」という「集約ルート」に変更依頼をすることができます。集約内部にある「注文明細」や「配送先住所」といった集約ルート以外のオブジェクトに直接指示を出すことはありません。
集約が担う境界内の「整合性」
集約はオブジェクト群の整合性の境界を保ちながらデータを更新します。整合性には以下の2つが存在しています。
- トランザクション整合性
- 結果整合性(4章「CQRSと結果整合性」参照)
DDDの集約は、即時的な整合性である「トランザクション整合性」と同義です。このことから、トランザクションの分析をした後でなければ、集約の設計が適切かを判断できません。なお、トランザクションという言葉は使っていますが、特定のDBのトランザクション命令そのものではありません。あくまで(結果整合性とは対照的に)同期的に整合性を保つ必要性を表しています。
上の注文例の場合、注文明細として100円の追加をした場合には、注文合計額にも100円が同時に加算された上で確定される必要があります。
IDDDのサンプルプロジェクトで考える集約
それでは、SaaSOvationを例に集約について見ていきましょう。前回まで紹介してきた通り、アジャイルプロジェクトのサービスに関するモデリングを見ていきます。ここでは、各プロダクトは複数のバックログアイテム(課題)を持ちます。そしてスプリント(2週間などのタイムボックス期間)やリリース(デプロイ日)を持ちます。これらを集約の観点からモデリングしてみます。
巨大な集約の問題点
まず、何も考慮せずにモデリングした場合、大きな集約ができ上がります。こうした大きい集約「プロダクト」を作成した場合、数千のバックログアイテム、スプリント、リリースオブジェクトを毎回メモリに読み込み、更新する必要があり、性能面で期待することはできません。
モデリングは簡単ですが、この場合、実用に耐えません。複数のユーザーが同時に操作した場合に、トランザクションが衝突する可能性が非常に高いためです。通常、エンティティの実装を行う場合、楽観的並行性制御と呼ばれる手法がよく用いられます。この手法では、エンティティがバージョン番号を保持しており、なんらかの変更を行う度にバージョン番号を加算していきます。もし別のユーザーが集約を更新済みの場合、想定しているバージョン番号と異なるため、後から更新しようとしたユーザーがエラーとなります。そのため別のユーザーに更新された後の最新値を取得して、更新処理をやり直す必要があります。
小さな集約の特徴
そこで、大きな集約の問題を回避するために、小さい集約を複数作成することにします。これまでひとつだった集約を複数に分解します。各集約それぞれに集約ルートが存在することになります。
この例では、大きな集約を分割し、バックログアイテムの集約にフォーカスしています。プロダクト/リリース/スプリントといったエンティティは、別の集約のルートエンティティとしてモデリングしています。そして、他の集約ルートを直接参照するのではなく、識別子(ID)のみを参照しています。トランザクションは、集約を取り扱うアプリケーションサービス(11章)側で制御することになります。大きな集約と小さな集約の違いについて簡単に見てみましょう。
項目 | 大きい集約 | 小さい集約 | |
---|---|---|---|
トランザクションの衝突 | 多い | 少ない | |
設計難易度 | 低 | 中 | |
スケーラビリティ・性能 | 低 | 高 | |
エンティティ間の依存 | 直接参照 | ID参照 |
このように、集約を小さくすることでトランザクションが衝突しなくなります。なお、要件を満たすだけの性能が出せるかは、実際のシナリオに基づいた検証を行う必要があります。参考までに、IDDD本の中では、ひとつの集約が144個のオブジェクトを保持することを試算し、ここでは問題ないと判断しています。
[コラム]DDD本における集約の解説
DDD本でEvans氏は
ほとんどのビジネスドメインは非常に強く相互に結びついているので、結局はオブジェクトの参照を通じて、長くて深い経路を辿ることになる。ある意味で、こうしたもつれはこの世界の現実を反映している。現実には、はっきりした境界が引いてもらえることはめったにないのだ。
と述べ、ドメインが大きくなることは自然な傾向だと述べています。それをふまえた上で、トランザクションや性能面の問題に対応する現実的な手法として集約を以下の通り定義しています。
エンティティと値オブジェクトを集約の中にまとめ、各集約の周囲に境界を定義すること。各集約に対してルートとなるエンティティを1つ選び、境界の内部に存在するオブジェクトへのアクセスは、そのルートを経由して制御すること。外部のオブジェクトが参照を保持できるのは、ルートのみとすること。内部のメンバに対する一時的な参照を渡してよいのは、単一の操作で使用する時だけだ。ルートがアクセスを制御するので、内部が知らないうちに変更されることはなくなる。この取り決めにより、どんな状態変化においても、集約内にあるオブジェクトと集約全体に対して、不変条件をすべて強制することが現実的になる。