Value Object
Value Objectは、識別子ではなく保持する値によって等価性が決まるオブジェクトです。金額と通貨、期間の開始日と終了日、緯度と経度のように、複数の値を一つの意味として扱う場合に有効です。
採用する場面
- 同じ検証、正規化、計算が複数箇所へ散らばっている
- 複数の値を常に一緒に扱わないと不変条件を破る
- 文字列や数値のままでは、許されない操作まで実行できる
- 値による等価比較がドメイン上の意味を持つ
入力境界で一度検証するだけの値や、振る舞いを持たないDTOまでクラスで包みません。型の取り違えだけを防ぐなら、ブランド型の方が小さく表現できます。
実装方針
- 不正な値を生成できる公開コンストラクタを避け、検証済みの生成経路を用意する
- 生成後の値は直接変更せず、変更が必要なら新しい値を返す
- 等価性は参照ではなく、意味を構成するすべての値で判定する
- シリアライズ形式と、外部値から再生成する検証経路を明確にする
- 表示用の文字列と、計算・保存に使う値を混同しない
単一の値を包むValue Objectは、getterと判定メソッドだけで十分です。複数の値を持つ場合に、変更後の新しい値を返すwithXxxメソッドを定義します。一部の値が省略可能になる場合は、オプショナルプロパティのまま持たせず、Union型で別の型へ分割します。
TypeScriptのreadonlyはコンパイル時の制約です。Object.freezeは実行時に変更を防ぎますが浅い凍結であり、ネストしたオブジェクトまで不変にはしません。どちらもValue Objectの定義そのものではなく、必要な不変性を実現する手段として選びます。
Entityとの違い
Entityは値が変わっても同じ識別子を持つ対象です。Value Objectは同じ値なら同じものとして扱えます。住所をValue Objectとして扱うか、住所録の一件をEntityとして扱うかは、利用する文脈によって変わります。