■ 1. 記事の目的と対象
- ValueObjectの不変性にフォーカスし、アンチパターンを提示した後、本来あるべき姿の例を記載する
- 対象: アプリケーションエンジニア、DDDに取り組んでいる人・取り組みたい人
- Vladimir Khorikov氏の言葉: 「ValueObjectを不変にできない場合は、それはValueObjectではない」
■ 2. ValueObjectとEntityの違い
- Entity: 可変・同一性を持つ(ライフサイクルを持つ)
- ValueObject: 不変・同一性を持たない(ライフサイクルを持たない)
- 同一性の例: 人物は20年経っても同じ人物として探すことができる(Entity)。名前や趣味は変わることがある(ValueObject)
■ 3. 比較(評価)の違い
- オブジェクトの評価方法には三種類ある:
- Reference equality: 参照の等価性
- Identifier equality: 識別子の等価性
- Structural equality: 構造的等価性
- EntityはユニークなIDで比較を行う(Identifier equality)
- ValueObjectは構造で比較を行う(Structural equality)
- Entity: 記憶という識別子を元に本人かどうかを評価する
- ValueObject: 名前(文字列)での構造的評価を行うため、ヒットしたアカウントが複数の場合もある
■ 4. アンチパターン1: 単なる型定義
- この実装では型でドメインを表現できていると満足しているが、userIDもEmailもドメインの不変性という責務を果たせていない
- ValueObjectの核心的な責務は「ドメインの不変条件(invariant)を保証し、カプセル化すること」である
■ 5. 型定義だけの実装がアンチパターンな理由1: カプセル化の欠如
- Goの場合、型変換によってバリデーションを簡単にバイパスできる
- ファクトリ関数(コンストラクタ)を用意しても、パッケージ外から型変換で不正な値を強制的に作れてしまう
- アプリケーション全体でEmail型の値が常に「有効なメールアドレスである」ことを保証できない
- 他のパッケージから不正なメールアドレスのオブジェクトをインスタンス化できてしまう
■ 6. 型定義だけの実装がアンチパターンな理由2: ドメインの知識(振る舞い)の欠如
type Email stringの定義では、その型はstringとほぼ同等の能力(stringへの型変換が容易)しか持たない- ValueObjectは単なる「バリデーション済みの値」ではなく、その値に関連する「ドメイン固有の振る舞い(ロジック)」もカプセル化する責務を持つ
- 例: 「Emailアドレスからドメイン部分だけを取得したい」というドメインの要件があった際、現状の設計であれば実装がアプリケーション層に記載されることになる
- 「Emailのドメインパートのみを取得する」といったオブジェクトの振る舞いは関心の分離を行う対象であり、アプリケーション層ではなくドメイン層で設計・コーディングすることがオーソドックスなスタイルである
■ 7. 正しい実装: カプセル化と不変性を強制
- 構造体としてValueObjectを表現し、値と振る舞いの両方をカプセル化する
■ 8. アンチパターン2: 不変性の欠如
- ValueObjectがポインタレシーバを持つ場合、「値が変更可能であることを示唆する」表現となるため、不変性を破壊しかねない
- 基本的に不変なものは値レシーバが推奨される
■ 9. 実装のポイント
- ValueObjectを構造体として表現し、構造体が持つ値への参照を非公開(value)にしてgetter経由でしか参照させないようにすることで、パッケージ外からの型変換や直接的なフィールド代入を防ぐ
- インスタンス化をNewEmailに強制し、バリデーションを担保する
- ValueObjectの関数は値レシーバとし、不変性を明確にする
■ 10. まとめ
- 「ただの型定義」は、ドメインルールを強制できない「なんちゃってValueObject」である
- 不変性が必要な場面ではカプセル化を徹底し、ドメインルールを守らないオブジェクトの生成を許さないような設計を心がける必要がある
- ファクトリー関数を通して、ドメインルールが守られたインスタンスが生成される
- そうすることで初めて、ValueObjectは「意味のある」存在となる
- 結論: 「ValueObject導入するなら、値も振る舞いもカプセル化してくれ」