PRACTICAL GUIDE

ドメインモデルの作り方

ドメインモデルは、具体例を通して業務を理解し、そこで使う言葉、守るルール、システムが担う判断を整理して作ります。

業務に登場するもの、それらの関係、守るルールや判断を、開発に関わる人たちが共有できる形で表したものがドメインモデルです。

決まった一つの手順があるわけではありません。今回は、フットサルNOWの「参加者募集」を題材にドメインモデルを作ります。主催者が募集を作り、参加希望者が申し込むまでを扱います。

具体例で整理する

まず、実際に起こりそうな例を一つ作ります。誰が何をするのか、定員は何人かを具体的にすると、「申し込んだ時点で参加は決まるのか」「定員が埋まっていたらどうなるのか」など、確認すべきルールが見えてきます。

主催者が、今日20時開催、定員2人、申込期限18時の募集を作る。

player-1が17時に、その募集へ参加を申し込む。

この例から、次のことが分かります。

  • ・募集は主催者が作る
  • ・募集には開催日時、定員、申込期限がある
  • ・参加希望者は募集へ参加を申し込む

また、具体例を出したことで、募集に必要な情報や申込みのルールについて、まだ分からないことも見えてきます。例えば、次のようなことです。

  • ・どこで開催するのか
  • ・参加費はいくらか
  • ・男性と女性のどちらも参加できるのか
  • ・参加できる年齢に条件はあるか
  • ・申込期限を開催日時より後に設定できるか
  • ・主催者自身も参加を申し込めるか
  • ・申込期限を過ぎても申し込めるか
  • ・同じ募集へ二重に申し込めるか

これらの疑問を主催者に確認します。ここでは、「募集には開催場所が必要」「申込期限を過ぎたら申し込めない」「同じ募集へ二重に申し込めない」と分かったものとして進めます。

言葉の意味を揃える

業務に詳しい人(ドメインエキスパート)や開発者など、開発に関わる人たちの認識を揃えるために、会話に出てきた言葉の意味を確認します。同じ言葉でも、人によって捉え方が異なることがあるためです。

DDDでは、このように開発に関わる人たちが共通して使う言葉をユビキタス言語と呼びます。

先ほどの具体例では、「募集」「参加申請」「申込期限」という言葉が使われていました。それぞれの意味と、コードで使う名前を整理すると、次のようになります。

募集

RecruitmentEntity

主催者が開催日時、場所、定員、申込期限を決めて参加者を募るもの。

参加申請

requestParticipation()

参加希望者が、参加したい募集に対して行う操作。

申込期限

participationRequestDeadline

その募集へ参加を申し込める期限。

コードで表す

整理した言葉をクラスやメソッドの名前に使い、確認したルールをコードに表します。例えば、「申込期限を過ぎたら申し込めない」「同じ人は二重に申し込めない」と分かった場合は、次のように表せます。

TypeScript

// 「募集」の情報と、募集が守るルールを表すクラス
class RecruitmentEntity {
  // 参加が確定した人のID
  private participantIds: string[] = []

  constructor(
    // 募集を作った主催者
    readonly organizerId: string,

    // フットサルを開催する日時
    readonly eventDateTime: Date,

    // フットサルを開催する場所
    readonly location: string,

    // 参加を申し込める期限
    readonly participationRequestDeadline: Date,

    // 参加できる人数
    readonly capacity: number,
  ) {}

  // 参加を申請する人のIDと、申請時刻を受け取る
  requestParticipation(requesterId: string, now: Date) {
    // 申込期限を過ぎたら申し込めない
    if (now > this.participationRequestDeadline) {
      throw new Error("申込期限を過ぎています")
    }

    // 同じ人は二重に申し込めない
    const alreadyRequested = this.participantIds.includes(requesterId)
    if (alreadyRequested) {
      throw new Error("すでに参加申請しています")
    }

    // 条件を満たした場合は、その場で参加を確定する
    this.participantIds.push(requesterId)
  }
}

この例では、話し合って整理した「募集」の情報と申込みのルールを、RecruitmentEntityクラスで表しています。新しいルールや認識の違いが見つかったら、整理した内容とコードの両方を見直します。

※ DDDで必須の命名ではありません。このガイドラインでは、ファイル名やクラス名だけで役割を見分けやすくするため、EntityとValueObjectを名前に付けています。Aggregate RootはEntityに与えられる役割なので、名前には付けません。