Application Service

DDDでの役割

Application Serviceは、画面やAPIから受けた要求に対して、Repositoryやドメインモデルを必要な順番で呼び出し、参加申請などのユースケースを実行します。業務上の判断は、Entity、Value Object、Domain Serviceが行います。

具体例で整理する

フットサルNOWの「参加申請」をユースケースとして整理します。参加者が募集の「参加する」を押すと、対象の募集を取得し、開催時間の重複を確認し、募集へ参加者を追加して、変更した募集を保存します。

この一連の処理をApplication Serviceが進めます。

コードで表す

前のページで扱った、開催時間の重複を確認するルールも追加した場合のコード例です。

このガイドラインでは、参加申請ユースケースの呼び出し口をIRequestParticipationApplicationServiceとして定義します。interfaceの詳しい説明は次のページで行います。

// RequestParticipationApplicationService.ts
import type {
  IRequestParticipationApplicationService,
} from "./IRequestParticipationApplicationService";
import type { RequestParticipationCommand } from
  "./RequestParticipationCommand";
import type { IRecruitmentRepository } from
  "../domain/recruitment/IRecruitmentRepository";

// 参加申請のユースケースを実行するApplication Service
export class RequestParticipationApplicationService
  implements IRequestParticipationApplicationService {
  // 募集の取得と保存に使うRepository
  private readonly recruitmentRepository: IRecruitmentRepository;

  // 複数の募集で開催時間が重なっていないか確認するDomain Service
  private readonly participationScheduleDomainService:
    ParticipationScheduleDomainService;

  // このユースケースで使うRepositoryとDomain Serviceを受け取る
  constructor(input: {
    recruitmentRepository: IRecruitmentRepository;
    participationScheduleDomainService: ParticipationScheduleDomainService;
  }) {
    this.recruitmentRepository = input.recruitmentRepository;
    this.participationScheduleDomainService =
      input.participationScheduleDomainService;
  }

  // 参加申請のユースケースを実行する
  async execute(command: RequestParticipationCommand): Promise<void> {
    // 1. 参加したい募集を取得する
    const recruitment = await this.recruitmentRepository.findById(
      command.recruitmentId,
    );

    // 指定されたIDの募集がなければ、参加申請を続けない
    if (!recruitment) {
      throw new Error("募集が見つかりません");
    }

    // 2. その人の参加が確定している募集を取得する
    const confirmedRecruitments =
      await this.recruitmentRepository.findConfirmedByParticipant(
        command.participantId,
      );

    // 3. Domain Serviceに開催時間の重複を確認してもらう
    const hasConflict = this.participationScheduleDomainService.hasConflict({
      requestedRecruitment: recruitment,
      confirmedRecruitments,
    });

    // 開催時間が重なっていれば、参加申請を続けない
    if (hasConflict) {
      throw new Error("開催時間が重なる募集には参加できません");
    }

    // 4. RecruitmentEntityに参加申請を受け付けてもらう
    recruitment.requestParticipation({
      requesterId: new ParticipantIdValueObject(command.participantId),
      now: command.requestedAt,
    });

    // 5. 参加者が追加された募集を保存する
    await this.recruitmentRepository.save(recruitment);
  }
}

RequestParticipationApplicationServiceが決めているのは、どの処理をどの順番で行うかです。開催時間の重複はDomain Serviceが判断し、申込期限や定員などのルールはRecruitmentEntityが確認します。

IRecruitmentRepositoryの詳しい役割は、「Repository interface」で説明します。

Application Serviceを呼び出す例

ここでは、APIからApplication Serviceを呼び出す例を示します。参加者が画面で「参加する」を押すと、APIが参加申請の情報を受け取り、IRequestParticipationApplicationServiceを通してApplication Serviceを呼び出します。

// APIで参加申請のCommandを作る
const command: RequestParticipationCommand = {
  recruitmentId,
  participantId: signedInUser.id,
  requestedAt: new Date(),
};

// 参加申請のApplication ServiceへCommandを渡す
await requestParticipationApplicationService.execute(command);

APIは、RepositoryやDomain Serviceを直接呼びません。参加申請の開始点をIRequestParticipationApplicationServiceに揃えることで、同じ処理の流れを使えます。

ディレクトリで表す

Application Serviceはapplication/に置き、利用するドメインモデルはdomain/に置きます。

src/features/futsal/
├── application/
│   ├── RequestParticipationCommand.ts               ← Command
│   ├── IRequestParticipationApplicationService.ts  ← interface
│   └── RequestParticipationApplicationService.ts   ← 実装
└── domain/
    └── recruitment/
        ├── RecruitmentEntity.ts
        ├── ParticipantIdValueObject.ts
        └── ParticipationScheduleDomainService.ts

※ interfaceを作ることや、一つの機能ごとにクラスを分けることは、DDDの必須ルールではありません。このガイドラインでは、interfaceの先頭にIを付け、実装と見分けられるようにしています。