Value Object

Value Objectとは

Value Objectは、申込期限や金額など、業務で意味を持つ値を表すドメインモデルの構成要素です。値が正しいかを確認するルールと、その値に関する判断をまとめます。

Entityが識別子で区別されるのに対し、Value Objectは持っている値で区別されます。たとえば、日時が同じ二つの申込期限は、同じ申込期限として扱います。

値で区別する

例えば、1,000円という金額は、どこで作られたかに関係なく同じ1,000円です。個別のIDは必要なく、金額と通貨が同じなら同じ金額として扱います。

Value Objectの特徴

値が同じなら同じものとして扱う
二つのValue Objectが同じかどうかは、識別子ではなく、持っている値で判断します。
作った後は値を書き換えない
申込期限を18時から19時へ変更する場合、18時の申込期限を書き換えるのではなく、19時の申込期限を新しく作って置き換えます。
値の振る舞いをまとめる
値の振る舞いとは、その値を使って行う判断・比較・計算などです。これらをValue Objectにまとめます。

具体例で整理する

前のページで見つけた「申込期限」を、Value Objectとして表します。申込期限は識別子ではなく日時によって区別し、日時が同じなら同じ申込期限として扱います。

申込期限には、「有効な日時である」という条件があります。また、期限を過ぎているか、指定した日時と同じかそれより後か、別の申込期限と同じ日時かを判断する振る舞いがあります。こうした条件と振る舞いを、申込期限のValue Objectとしてまとめます。

値

2026-08-11 18:00

満たす条件

有効な日時である

値の振る舞い

  • 現在時刻が期限を過ぎているか判断する
  • 指定した日時と同じか、それより後か判断する
  • 別の申込期限と同じ日時か判断する

コードで表す

申込期限をValue Objectとして表すと、例えば次のようになります。

TypeScript

// 「申込期限」を表すValue Object
class ParticipationRequestDeadlineValueObject {
  private readonly value: Date

  constructor(value: Date) {
    // 有効な日時か確認する
    if (Number.isNaN(value.getTime())) {
      throw new Error("申込期限には有効な日時を指定してください")
    }

    // 外から渡されたDateを複製し、後から変更されないようにする
    this.value = new Date(value)
  }

  // 現在時刻が申込期限を過ぎたか判断する
  hasPassed(now: Date) {
    return now > this.value
  }

  // 指定した日時と同じか、それより後か判断する
  isOnOrAfter(date: Date) {
    return this.value >= date
  }

  // 日時が同じなら、同じ申込期限と判断する
  equals(other: ParticipationRequestDeadlineValueObject) {
    return this.value.getTime() === other.value.getTime()
  }
}

EntityでValue Objectを使う

募集を表すEntityのRecruitmentEntityでは、申込期限にParticipationRequestDeadlineValueObjectを使います。期限を過ぎているかなど、申込期限に関する判断はParticipationRequestDeadlineValueObjectが行います。

TypeScript

// RecruitmentEntityの一部を抜粋
class RecruitmentEntity {
  constructor(
    private readonly participationRequestDeadline: ParticipationRequestDeadlineValueObject,
    eventDateTime: Date,
  ) {
    // 「申込期限は開催日時より前」という募集のルールを守る
    if (participationRequestDeadline.isOnOrAfter(eventDateTime)) {
      throw new Error("申込期限は開催日時より前に設定してください")
    }
  }

  apply(now: Date) {
    // 日時を直接比較せず、申込期限に判断を任せる
    if (this.participationRequestDeadline.hasPassed(now)) {
      throw new Error("申込期限を過ぎています")
    }

    // 参加申請を受け付ける
  }
}

ParticipationRequestDeadlineValueObjectは日時の検証や比較を担当します。RecruitmentEntityはその振る舞いを使い、「申込期限は開催日時より前」「期限を過ぎたら申し込めない」という募集のルールを守ります。

フットサルNOWでの配置例

src/features/futsal/
└── domain/                      ← ドメインモデルを置くディレクトリ
    ├── recruitment/
    │   ├── RecruitmentEntity.ts ← Aggregate Root(Entity)
    │   ├── RecruitmentStatusValueObject.ts ← Value Object
    │   ├── RecruitmentCapacityValueObject.ts ← Value Object
    │   ├── ParticipantIdValueObject.ts ← Value Object
    │   └── ParticipationRequestDeadlineValueObject.ts ← Value Object
    └── ...                      ← ほかのドメインモデル

※ これはフットサルNOWでの配置例です。DDDが特定のディレクトリ構成を定めているわけではありません。

※ このガイドラインでは、ファイル名やクラス名だけで役割を見分けやすくするため、DDD上の役割を名前に付けています。