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を名前に付けています。