Aggregate Root
Aggregate Rootとは
Aggregate Rootは、Aggregateを代表するEntityです。Aggregateの状態を変えるときは、Aggregate Rootのメソッドを呼びます。Aggregate Rootは、必要な業務ルールを確認してから処理します。
Aggregate Root専用の種類があるわけではありません。Aggregate内のEntityの一つが、この役割を担います。
具体例で整理する
前のページでは、「募集」を一つのAggregateとして扱う範囲を決めました。このAggregateに含まれるEntityは「募集」だけです。そのため、「募集」がAggregate Rootになります。
Aggregateによっては、複数のEntityが含まれることもあります。その場合は、Aggregate全体への操作を受け付けるEntityを一つだけAggregate Rootにし、ほかのEntityはAggregate内部のEntityとして扱います。
「募集」は、「参加を申請する」「開催場所を変更する」「募集を締め切る」といった操作を受け付け、必要なルールを確認してから処理します。
例えば、今日20時開催、定員2人、申込期限18時の募集に、参加希望者が17時に参加を申請したとします。現在の参加者は1人で、この人からの申請はまだありません。
システムは、対象となる「募集」に参加申請を依頼します。「募集」がルールを確認し、参加できる場合だけ参加者を追加します。
1 参加を申請する
参加希望者が17時に申請
2 募集がルールを確認する
募集
Aggregate Root
- ・申請時刻17時は、申込期限18時より前
- ・同じ人の参加申請はない
- ・現在の参加者は1人で、定員2人に達していない
3 参加を確定する
参加者として募集へ追加
参加者2人/定員2人
なぜAggregate Rootから操作するのか
参加申請をAggregateの外から直接追加できると、申込期限や定員を確認せずに受け付けられてしまいます。参加申請の受付を「募集」に任せれば、参加申請を受け付けるたびに同じルールを確認できます。
コードで表す
実際のRecruitmentEntityでは、参加申請を受け付ける処理を次のように書いています。
// 「募集」のEntityがAggregate Rootの役割を担う
export class RecruitmentEntity {
requestParticipation(input: {
requesterId: ParticipantIdValueObject;
now: Date;
}): void {
// 受付中の募集でなければ、参加申請を受け付けない
this.assertOpen();
// 開催日時を過ぎていたら、参加申請を受け付けない
if (input.now >= this.props.eventDateTime) {
throw new RecruitmentRuleError(
"EVENT_ALREADY_STARTED",
"開催済みの募集には参加申請できません",
);
}
// 申込期限を過ぎていたら、参加申請を受け付けない
if (this.props.participationRequestDeadline.hasPassed(input.now)) {
throw new RecruitmentRuleError(
"PARTICIPATION_REQUEST_DEADLINE_PASSED",
"申込期限を過ぎています",
);
}
// 主催者自身からの参加申請は受け付けない
if (input.requesterId.value === this.props.organizerId) {
throw new RecruitmentRuleError(
"ORGANIZER_CANNOT_REQUEST_PARTICIPATION",
"主催者自身は参加申請できません",
);
}
// 同じ人の参加申請があれば、二重に受け付けない
if (
this.participantIdValues.some((participantId) =>
participantId.equals(input.requesterId),
)
) {
throw new RecruitmentRuleError(
"DUPLICATE_PARTICIPATION_REQUEST",
"同じ募集へ二重に参加申請できません",
);
}
// 定員に達していたら、参加申請を受け付けない
if (this.props.capacity.isReached(this.confirmedParticipantCount)) {
throw new RecruitmentRuleError(
"CAPACITY_REACHED",
"定員に達している募集には参加申請できません",
);
}
// 条件を満たした場合は、その場で参加が確定する
this.participantIdValues.push(input.requesterId);
}
}requestParticipation()の中で参加者を追加することで、ルールを確認せずに参加が確定することを防ぎます。
補足:Entityが複数ある場合
募集Aggregateでは、EntityはRecruitmentEntity一つだけでした。しかし、Aggregateに含まれるEntityが、常に一つとは限りません。
例えば注文では、「注文」と、その注文に含まれる複数の「注文明細」を一つのAggregateとして扱います。注文明細は一件ずつ明細IDで区別し、数量が変わっても同じ明細として管理するため、それぞれをOrderLineEntityとして表します。この場合、注文AggregateにはOrderEntityと複数のOrderLineEntityが含まれます。
Entityが複数あっても、Aggregate Rootは一つです。注文は、複数の注文明細を含めて一つの単位として作成・確定・取消します。また、注文明細だけでは、注文が確定済みか、ほかの明細を含めた合計数量や金額がいくつかを確認できません。
そのため、注文全体の情報を持ち、「商品を追加する」「明細の数量を変更する」「注文を確定する」といった操作を受け付けるOrderEntityをAggregate Rootにします。明細の数量を変更するときも、OrderEntityが注文全体のルールを確認してからOrderLineEntityを変更します。
ディレクトリで表す
src/features/futsal/
└── domain/
└── recruitment/
├── RecruitmentEntity.ts ← Aggregate Root
├── RecruitmentStatusValueObject.ts
├── RecruitmentCapacityValueObject.ts
├── ParticipantIdValueObject.ts
└── ParticipationRequestDeadlineValueObject.ts※ Aggregate RootはEntityに与えられる役割なので、クラス名にはAggregateRootを付けず、注釈で示しています。