0. はじめに
ValueObjectって"不変性"や"同一性を持たない"という点が特徴ですよね。
あなたのドメインモデリングは、その特徴を消した実装になっていませんか?
ソフトウェア業界で著名な Vladimir Khorikov 氏は以下のように言及されています。
if you can’t make a value object immutable, then it is not a value object.
訳: ValueObject を不変にできない場合は、それは ValueObject ではない。
本記事では ValueObject の不変性にフォーカスし、
アンチパターンを提示した後、本来こうあるべきでは?
という例を記載させていただきます。
※ 今回は、書籍なり記事なりを読んで真面目に書いてみました。
それでは!どうぞっ!
先にまとめ
わりと長くなったので、まとめを置いときます。
「VO 導入するなら、値も振る舞いもカプセル化してくれ」
1. 対象
- アプリケーションエンジニア
- DDDに取り組んでる人・取り組みたい人
※ 本記事では「Entityとはなにか」「DDDとはなにか」については、主題からズレますので紹介しません。
※ 本記事ではGo言語で記載していますが、型定義や簡単な関数のみです。ライトに読めます。
2. VO(ValueObject)とは
簡単にですが、概要を図で紹介させていただきます。
簡単に言うとこんな感じでしょうか。
- Entity: 可変・同一性を持つ(ライフサイクルを持つ)
- ValueObject: 不変・同一性を持たない(ライフサイクルを持たない)
同一性ってなに?
「同一性ってちょっとしっくり来ないな」って人もいるかなと思うので、ちょっと深掘りますね。
※ "同一" = "ユニーク" とは異なります。
昔作った図があるのでそちらを使わせていただきますね。
この例だと、5歳の時に「絶対結婚しような」と僕、にプロポーズしてきた女の子が、
20年たって「そういえば彼は今なにしてるんだろう。」とSNSなり知人なりで僕を探します。
(※ まぁそんなことはありませんが。)
この際、僕、umekikazuya は 20年たった今でも
あの時の umekikazuya との 同一性 が担保されてますので、探してればいつか見つかります。
つまり、今回の例において、人物を Entity と捉えることが出来ますね。
ただ、ValueObject については同一性を持たないというのが特徴です。
ちょっと例えが難しいのですが、
Entity を人物と表現した今回のケースで言うと、「名前」や「趣味」などを思っていただければいいかな、と思います。
名前も趣味も変わることありますからね。
(updateHobby(input string)関数等で、Entity のフィールドを更新するイメージですね)
比較(評価)の違いについて
セクションタイトル、ちょっとわかりづらいですが、
Entity と ValueObject では評価(比較)の違いも大きく異なります。
オブジェクトの評価方法には、三種類の等価方法があります。
- Reference equality: 参照の等価性
- Identifier equality: 識別子の等価性
- Structural equality: 構造的等価性
上記のうち、本記事で扱うのは 2つ目・3つ目 です。
The concept of identifier equality refers to entities, whereas the concept of structural equality - to value objects.
つまり、
Entity はユニークなIDで比較を行う。ValueObject は構造で比較を行う。
といったものです。
これ、どういうことかと言いますと、
先程の「絶対結婚しような」の例で言うと、
Entity の場合
〇〇という女の子が umekikazuya を探して見つけました。
実際に本人(umekikazuya)と会って、本人かどうかを照合するために、「記憶という identifier」を元に本人かどうかを二人で評価します。
VO の場合
例が少し難しいので、2つほど提示しますね。
例: その①
〇〇という女の子が umekikazuya をSNSで文字列で探します。
今回は、IDではなく "名前(文字列)" での構造的評価ですので、ヒットしたアカウントが複数の場合もありますよね。
例: その②
〇〇という女の子が umekikazuya を探して、実際に本人とあって、本人である照合を行いました。
思い出話に花を咲かせている中で、「〇〇幼稚園だったよね?」という質問を〇〇が問いかけます。
その際、アルバムなり記憶なりどこかから持ってきた、「〇〇幼稚園という文字列そのもの を照合」し、二人で幼稚園の名前の整合について評価をします。
こんな感じでしょか。
若干、解釈が異なる人いるかもしれません(僕が違うかも。コメント下さい。)
ざつーな例も混じえさせていただきました。
概要理解についてはこの辺にさせていただきます。
では、アンチパターンを入れつつ、実装例を 2つ ほど見ていきましょ。
3. あなたのVO、こうなっていませんか? ~ カプセル化の不徹底
前提: Userの Entity 定義
アンチパターンの紹介に入る前に、
軽く今回扱うEntityのドメインモデリングを前提として提示しておきます。
type User struct {
id UserID
email Email
}
idフィールド と emailフィールド を持っているシンプルなエンティティです。
各属性を生の string ではなく ValueObject 化しているいい例ですね。
詳細は触れませんが、この段階でのアンチパターンも一応記載しますね。
一応 VO 導入していない例を記載
type User struct {
- id string
- email string
+ id UserID
+ email Email
}
こう書いている VO 見かけませんか?
type UserID string
type Email string
これを見て「型でドメインを表現できている」と満足(判断)していませんか?
残念ですが、今回の VO は userID も Email もドメインの不変性という責務を果たせていません。
なぜその VO は意味がないの?
VOの核心的な責務は「ドメインの不変条件 (invariant) を保証し、カプセル化すること」です。
type定義だけの実装がアンチパターンな理由は2つあります。
理由1: カプセル化の欠如(誰でも不正な値を作れる)
type Email string のような定義は、Goの場合は「型変換」によってバリデーションを簡単にバイパスできます。
func NewEmail(addr string) (Email, error) {
if !isValidEmail(addr) { // バリデーション処理
return "", errors.New("invalid email")
}
return Email(addr), nil
}
// ファクトリ関数(コンストラクタ)を用意しても、
// パッケージ外から、型変換で不正な値を強制的に作れてしまう
invalidEmail := email.Email("ただの文字列")
これだと、アプリケーション全体でEmail型の値が常に「有効なメールアドレスである」ことを保証できないです。
他のパッケージから、不正なメールアドレスのオブジェクトをインスタンス化できてしまいます。
理由2: ドメインの知識(振る舞い)の欠如
type Email string の定義では、その型は string とほぼ同等の能力(string への型変換が容易)しか持ちません。
VOは単なる「バリデーション済みの値」ではなく、その値に関連する「ドメイン固有の振る舞い(ロジック)」もカプセル化する責務を持ちます。
例えば、「Emailアドレスからドメイン部分だけを取得したい」というドメインの要件があった際に、現状の設計であれば、実装が記載される場所は、アプリケーション層となりますよね。
こういう「Emailのドメインパートのみを取得する」といった "オブジェクトの振る舞い" は関心の分離を行う対象です。
アプリケーション層ではなく、ドメイン層で設計・コーディングすることがオーソドックスなスタイルです。
じゃあどうするの?
リファクタしていきましょう。
「カプセル化」と「不変性」を強制する実装が必要です。
構造体として該当の VO を表現し、値と振る舞い、どちらもカプセル化しましょう。
type Email struct {
value string
}
// NewEmail は公開されたファクトリ関数
func NewEmail(in string) (Email, error) {
err:= Email{value: in}.validate()
if err != nil {
return Email{}, errors.New("invalid email")
}
return Email{value: in}, nil
}
// Value はgetter関数
func (e Email) Value() string {
return e.value
}
// GetDomain はメールアドレスのドメインパートのみを取得する関数(簡易なロジックだけどご容赦ください)
func (e Email) getDomain() string {
parts := strings.Split(e.value, "@")
if len(parts) == 2 {
return parts[1]
}
// (不変条件で形式は保証されているはず)
return ""
}
4. あなたのVO、こうなっていませんか? ~ 不変性の欠如
(これは Golang ならでは、って感じもありますが)
関数をポインタレシーバとするか、値レシーバとするか、迷う時よくありますよね。
基本的に、不変なものは値レシーバが推奨と私は考えています。
- // 悪い例 (ポインタレシーバ)
- func (input *Email) validate() error { ... }
+ // 良い例 (値レシーバ)
+ func (input Email) validate() error { ... }
VOがポインタレシーバを持つ場合、「値が変更可能であることを示唆する」ような表現となるため、
不変性を破壊しかねないですよね。
ポイントまとめますね。
ポイント:
- ValueObjectを構造体として表現し、
構造体が持つ値への参照を非公開(value)に getter 経由でしか参照させないようにすることで、
パッケージ外からの型変換や直接的なフィールド代入を防ぐ。 - インスタンス化を
NewEmailに強制し、バリデーションを担保する。 - VOの関数は値レシーバ(
e Email)とし、不変性を明確にする。
5. まとめ
「ただの型定義」は、ドメインルールを強制できない「なんちゃってVO」だと思っています。
不変性が必要な場面ではカプセル化を徹底し、
ドメインルールを守らないオブジェクトの生成を許さないような設計を心がけれるとよいかなぁと思っています。
ファクトリー関数を通して、ドメインルールが守られたインスタンスが生成されます。
そうすることではじめて、VOは「意味のある」存在となると思ってます。
みんなで VO を守りましょうね。
以上!あざしたっ!
6. 参考
私は以下の記事が参考になりました。
また、ちゃんと学びたいなら以下の書籍がおすすめです (まだ僕も途中までしか読めてないですが) 。

