集約の設計におけるルール
それでは、集約を設計するにあたっての基本的なルールについて紹介していきます。
1.小さな集約を設計する
先ほどの例で述べたように、パフォーマンスやスケーラビリティといった非機能要件の観点から、小さな集約になるように設計を行います。
2.極力、値オブジェクトから構成する
集約の内部は、ルートエンティティ以外は「値オブジェクト」だけであることが望ましいとされています。IDDD本の中で紹介されているプロジェクトでは、7割の集約はルートエンティティに値オブジェクトのプロパティを数個持たせるだけに抑えることができ、残り3割も2つか3つのエンティティを使用しただけとの報告があります。どうしても、ユースケース的に大きな集約を作らなければいけない場合は、結果整合性にて遅延を許容できないか検討してみるといいでしょう。
3.真の不変条件(ビジネスルール)を、整合性の中にモデリングする
集約には、守るべき「不変条件」があります。不変条件とは、どのような操作を行おうとも維持される「ビジネスルール」です。例えば「小計と消費税を足した値が合計となる」や「配送先住所が国外の場合は、電話番号に国コードを必須とする」といったものです。そのため、集約を設計する前に、まず境界づけられたコンテキスト内における不変条件を理解する必要があります。それを理解することで、集約にどのオブジェクトを含めることができるかを検討できます。集約ルートは、集約内のエンティティと値オブジェクトの値が正しいことを制御する責任を持ちます。
4.他の集約への参照へは、その一意な識別子を使用する
集約では、他の集約ルートへの参照を保持するようにします。密結合ではなく識別子(ID)を介した参照を使うことで、疎結合に集約を組み合わせることができます。一般的に複雑になるため双方向の参照も行いません。
重要な注意点として、単一のトランザクションでは単一の集約のみ更新します。複数の集約を更新しないようにします。どうしても複数の集約を更新しなければいけない場合は、結果整合性を用いた対策を検討する必要があります。
5.境界の外部では結果整合性を用いる
複数の集約にまたがるビジネスルールがある場合には、結果整合性を使うことになります。この場合、メインとなる集約でイベントを発生させ、受信した側の集約にて処理を継続します。この流れによって結果整合性が実現します。ドメインイベントには、イベントで処理するべき内容が把握できるように識別子が含まれます。
トランザクション整合性か結果整合性のどちらかの判断で悩む場合には、ユースケースを実行するユーザーが自分であれば「トランザクション整合性」を利用し、他のユーザーやシステムの役割と思える場合には「結果整合性」を使うことがヴァーノン氏によるひとつの指針となっています。
基本ルールに反する例外パターン
これまで、集約の設計に関する基本ルールを紹介してきましたが、あえて、トランザクションをひとつにまとめる場合もあります。それらについて見ていきましょう。
【例外1】ユーザーインターフェースの利便性を優先
画面の操作上、複数の集約をまとめて作成するようなシナリオの場合、集約を一括で作って操作しても問題ありません。
【例外2】技術的な仕組みの不足
結果整合性を実現するためのアーキテクチャが存在しない場合(イベントを扱うメッセージング基盤がない場合)には、集約を小さくすることが難しくなります。
【例外3】グローバルトランザクションの強制
既存のアーキテクチャ基盤や企業ポリシーにより、2フェーズコミットのようなグローバルトランザクションを使わなければいけない場合があります。極力複数の集約を更新しなくても問題ないように、トランザクションの衝突が発生しないようにします。
【例外4】クエリのパフォーマンス改善
他の集約を参照する場合、識別子で参照せずに直接参照したほうが性能が良いケースが考えられます。このような場合は直接参照します。
集約の実装コード例
設計方針と例外パターンに続けて、実際に、集約のコードについて見ていきましょう。
ルートエンティティの選択
まず、集約を作る場合、外部からの操作を担うひとつのエンティティを集約ルートとして選択します。ここでは、ルートエンティティである「Product」クラスのコード(C#/Java)を選択しています。
// 集約ルート(ルートエンティティ)「バックログアイテム」
public class BacklogItem : EntityWithCompositeId
{
// コンストラクタ
public BacklogItem(...)
{
(省略)
}
// 自分の一意な識別子(値オブジェクト)
public BacklogItemId BacklogItemId { get; private set; }
// 他集約「テナント」の一意な識別子(値オブジェクト)
public TenantId TenantId { get; private set; }
// 他集約「プロダクト」一意な識別子(値オブジェクト)
public ProductId ProductId { get; private set; }
// 「タスク」群の参照(エンティティ)
readonly List<Task> tasks;
// 自分のプロパティ「バックログアイテムタイプ」(値オブジェクト)
public BacklogItemType Type { get; private set; }
// 自分のプロパティ「バックログアイテムステータス」(値オブジェクト)
public BacklogItemStatus Status { get; private set; }
// 自分のプロパティ「カテゴリー」
public string Category { get; private set; }
}
集約ルートはエンティティなので、そのエンティティを判別するための一意な識別子を検討します。ここでは「ProductId」を識別子としています。
集約の部品を極力、値オブジェクトにする
設計ルールで紹介したように、集約の属性としては値オブジェクトを使うことが推奨されています。エンティティにするか値オブジェクトにするかを悩んだ場合、後々の交換が大変でなければ「値オブジェクト」を選択します。ただし、HibernateやEntity FrameworkといったO/Rマッピングツールの相性上、エンティティが望ましい場合はそれでも問題ありません。
なお、将来RDBMSからキーバリューストアに移行することになったとしても、エンティティから値オブジェクトに変更することはそれほど難しくありません(MongoDBのようなドキュメント指向データベース等では、集約自体をシリアライズしてひとつの値として格納します)。
なお、集約に入れたオブジェクトは他の集約で使ってはいけないと悩んだ方もいるかもしれませんが、そのような制約はありません。集約の内部に実装するエンティティも値オブジェクトも、他の集約から参照して問題ありません。
