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を付け、実装と見分けられるようにしています。