値オブジェクト(Value Object)は3種類ある
Value Object(値オブジェクト)は3種類あった
Value Object(値オブジェクト) の意義と使い所がわからなかった。そこで調べてみたらなんと3種類あった。面白かったのでその調査過程を紹介する。
なお、現在では DDD の意味での Value Object がメインであること、またこれは自転車置き場の議論であり、DDD Quickly の Value Object の章を読む方が有意義であることを先に記しておく。
1. Data Transfer Object
1つ目は、Data Transfer Object(DTO)の意味だ。これは PoEAA に少しだけだけ出てくる。かつてのJava界隈の一部では(?)DTOのことを Value Object と呼んでいた。だが、現代では Value Object と DTO は別物として定着している。PoEAA は2000年代前半に出版され、著者が序文で90年代の開発経験をまとめたと書いてるため、これは20年以上前の話だ。このため、このことはすぐ忘れてもいい。
まあ、DTO にはメソッドがなくプロパティという値だけが格納されている。それを Value Object と呼びたくなる気持ちもわかる。
2. Value Object パターン
2つ目は PoEAA で Value Object パターンと紹介されているものだ。これはオブジェクトのプロパティの値が意図せず上書きされてしまうのを回避するために考案されたパターンと理解している。その特徴は不変(イミュータブル)であること、int や number 型ではなく独自のクラスを定義していること、Value Object 同士の比較ができること、ハッシュ化できることだ。ただ、自分は PoEAA だけではよくわからなかった。
Value Object パターンは、ケント・ベックのテスト駆動開発 や 実装パターン という本にも登場する。後者ではお金を Value Object として扱うサンプルを紹介している。ドルを扱うために Dollar クラスを作ってみよう。コードは TypeScript で書き直している。
// マーカーインターフェース
interface Money {
getValue(): this
}
class Dollar implements Money {
constructor(private readonly value: number) {}
getValue() {
return this.value
}
// 最初から引数 other の型を Dollar とすれば条件分岐は不要
// だが、説明のためにあえて広い Money 型にしている
equals(other: Money) {
if (instanceof other !== 'Dollar') {
throw new InvalidArgsException('Variable other is not Dollar.')
}
return this.value === other.getValue()
}
}
Dollar クラスに setter はない。もし異なる値が必要なら新しい Dollar クラスを作成する。
const dollar = new Dollar(100)
dollar.setValue(200) // これはできない
// 不変にしておくと、値が後から上書きされる心配がない
const dollar100 = new Dollar(100)
const dollar200 = new Dollar(200)
// クラスは同じでも値は同じではない
dollar100.equals(dollar200) // false
ドルの Value Object 同士を計算するなら Transaction クラスを作ろう。これも例として本に掲載されている。
class Transaction {
constructor(
private readonly a: Dollar,
private readonly b: Dollar
) {}
add() {
const sum = a.getValue() + b.getValue()
return new Dollar(sum)
}
}
$100 + 100 yen
という式を書けばバグになることは間違いない。Dollar クラスを作ることで、Dollar と Yen を誤って足し算することを避けられる。Transaction クラスで型エラーを出すからだ。
// Value Object
const dollar = new Dollar(100)
const yen = new Yen(100)
// 型エラーになる
new Transaction(dollar, yen)
しかし、int や number のようなプリミティブ型なら計算できてしまう。
// プリミティブ型
const dollar: number = 100
const yen: number = 100
// 計算できてしまう
dollar + yen
バグの原因になること間違いなしだ。
ここから、Value Object パターンの本質は「不変性」と「計算可能性」だと理解した。計算可能性とは、同じクラスの値同士を計算できるという意味と、違うクラスの値は計算できないようにするという意味だ。クラスは単位と読み替えても良い。
Value Object パターンは数値に単位を与える。ドルと円を計算できないようにするのと同様に、「3時間 - 8人」のような単位の異なる値を計算できないようにする役割も担うものだと理解している。
3. DDD の Value Object
3つ目は、DDD の Value Object だ。2との違いは2点ある。
それは数値型以外も受け付けること。例えば User ID や文字列であるメールアドレスも DDD では Value Object とするそうだ。ここでは「計算可能性」の概念はなくなり、値の不変性のみが強調されている。
また、ドメイン話。Value Object はソフトウェアの中核を成すドメインオブジェクトと分類されている(以前書いたこちらの記事の図を参照)。しかし、2で紹介した PoEAA や テスト駆動開発といった本では、ドメインに関する言及はない。
自転車置き場の議論であることは認識する必要がある
個人的には以上のような理解をしている。Value Object を自分は理解していないという出発点から PoEAA を読んで理解しようと考えたのだが、なんだかややこしい。
普通 Value Object といえば3の意味だ。DDDの考案者である Eric Evans がこの実装パターンを Value Object と呼ばなければ混乱は生まれなかったのではと恨み言の一つでも言いたくなる。
元同僚でとても優秀なエンジニアが「Value Object の議論は自転車置き場の議論だよ」と話していてまさにその通りだなと思った。ただ、知っておくと面白い。
枝葉末節の議論は学者に任せて、自分たちは綺麗な動作するコードを書き、ユーザーに価値のあるソフトウェアを届けるという目標を忘れなければそれでいい。
というようなことを調べたり考えたりツイートしていた。しかし、全く同じ時期にかとじゅん(@j5ik2o)さん が「メモ:値オブジェクトの定義と差異について」という、Value Object の解説をしたこれ以上ないくらいの記事を公開されたため、本記事の下書きをお蔵入りにしていたのだった。
屋上屋を架すような内容ではあるが、最近また Value Object が話題になっているようなので清書して記事を公開することにする。
Happy Coding 🎉