Domain Service
Domain Serviceとは
業務ルールは、基本的にEntityやValue Objectで表します。しかし、複数の募集を比べる場合など、一つのEntityだけでは判断できないこともあります。
Domain Serviceは、そのような業務上の判断や処理を表す、ドメインモデルの要素です。
Domain Serviceが必要になる例
例えば、フットサルNOWに「同じ参加者は、開催時間が重なる二つの募集に参加できない」というルールを追加するとします。
このルールを確認するには、参加を申請する「募集」だけでなく、その人の参加がすでに確定している別の「募集」も必要です。一つの「募集」だけでは判断できません。
そこで、複数の募集の開催時間を比べる判断をDomain Serviceとして表します。
例えば、18:00〜20:00の募集へ参加を申請するとき、すでに19:00〜21:00の募集への参加が確定していれば、開催時間が重なるため参加できません。
コードで表す
次は、このルールを追加する場合のコード例です。二つの募集の開催時間を比べる処理を、どちらか一方の募集へ無理に持たせず、Domain Serviceに置いています。
// 同じ時間帯の募集へ重複して参加できないルールを確認する
export class ParticipationScheduleDomainService {
hasConflict(input: {
requestedRecruitment: RecruitmentEntity;
confirmedRecruitments: readonly RecruitmentEntity[];
}): boolean {
// 参加したい募集の開催時間が、参加確定済みの募集の
// いずれかと重なっていればtrueを返す
return input.confirmedRecruitments.some((confirmedRecruitment) =>
input.requestedRecruitment.eventSchedule.overlaps(
confirmedRecruitment.eventSchedule,
),
);
}
}Domain Serviceを呼び出す例
ここでは、Application ServiceからDomain Serviceを呼び出す例を示します。Application Serviceが必要な募集を取得し、開催時間が重なっていないことを確認してから、参加申請を受け付けます。
次は、Domain Serviceを呼ぶ部分を抜粋したコードです。RepositoryやDomain ServiceをApplication Serviceへ渡す方法は省略しています。
// Domain Serviceを呼ぶ部分だけを抜粋
export class RequestParticipationApplicationService {
async execute(
command: RequestParticipationCommand,
): Promise<void> {
// 参加したい募集を取得する
const recruitment = await this.recruitmentRepository.findById(
command.recruitmentId,
);
// 指定されたIDの募集がなければ、参加申請を続けない
if (!recruitment) {
throw new Error("募集が見つかりません");
}
// その人の参加が確定している募集を取得する
const confirmedRecruitments =
await this.recruitmentRepository.findConfirmedByParticipant(
command.participantId,
);
// Domain Serviceに開催時間の重複を確認してもらう
const hasConflict = this.participationScheduleDomainService.hasConflict({
requestedRecruitment: recruitment,
confirmedRecruitments,
});
if (hasConflict) {
throw new Error("開催時間が重なる募集には参加できません");
}
// 募集自身が持つ参加申請のルールを確認する
recruitment.requestParticipation({
requesterId: new ParticipantIdValueObject(command.participantId),
now: command.requestedAt,
});
// 参加者が追加された募集を保存する
await this.recruitmentRepository.save(recruitment);
}
}募集を取得することや、処理の順番を決めることはApplication Serviceの役割です。次のページでは、この役割を詳しく説明します。
ディレクトリで表す
このルールを実装する場合は、「募集」のドメインモデルと同じディレクトリへDomain Serviceを置きます。
src/features/futsal/
├── application/
│ ├── RequestParticipationCommand.ts
│ └── RequestParticipationApplicationService.ts ← Domain Serviceを呼ぶ
└── domain/
└── recruitment/
├── RecruitmentEntity.ts
├── ParticipationRequestDeadlineValueObject.ts
├── EventScheduleValueObject.ts
└── ParticipationScheduleDomainService.ts ← Domain Service※ このガイドラインでは、ファイル名とクラス名だけで役割を見分けられるようにDomainServiceを名前に付けています。