http://martinfowler.com/bliki/ValueObject.html

プログラミングをする時、物事を複合物として表現すると便利だと思うことがよくあります。 例えば、2次元座標はx軸とy軸で構成されます。お金の額面は数値と通貨で構成されます。日付の範囲は開始日と終了日で構成されます。日付は年、月、日で構成されることもあります。

これを実践してみると、2つの複合オブジェクトが同じものであるかどうかという疑問が湧いてきます。 例として、2つの点オブジェクトを考えてみましょう。それらは両方とも(2,3)のデカルト座標を示しています。この2つの点オブジェクトを等価として扱うことは理にかなっています。 プロパティの値が同じであるオブジェクト(この場合のプロパティはx座標とy座標)はバリューオブジェクトと呼ばれます。

しかしながら注意してプログラミングしないと、意図した動作にならない可能性があります。

例として、JavaScriptで点を表現したい場合

const p1 = {x: 2, y: 3};
const p2 = {x: 2, y: 3};
assert(p1 !== p2);  // 求めている結果ではない

残念ながら、このテストは通過してしまいます。 このようになってしまうのは、JavaScriptではオブジェクトが等価かどうかをテストする際、オブジェクトの参照を見にいく一方でオブジェクトに含まれている値を無視するためです。

大抵の場合、参照を利用することの方が値を利用することよりも理にかなっています。 例えば膨大な量の販売注文をロードし、コントロールしているとしましょう。各注文を一つの場所にロードすることは理にかなっています。その後、アリスの最後の注文が次回の配送に含まれるかどうかを確認する必要がある場合、アリスの注文のメモリ参照(もしくはID)を取得し、その参照が配送される注文一覧に含まれているかどうかを確認することができます。

したがって、オブジェクトの2つのクラス、バリューオブジェクトと参照オブジェクトは、それらをどのように区別するか、という観点で考えるのがいいと思います。1 各オブジェクトが等価性をどのように扱うかを確認し、プログラムすることで、期待通りに動作するでしょう。どうやってこれを実現するかは、利用しているプログラミング言語に依存します。

言語によっては複合データを値として扱うものもあります。Clojureで単純な複合データを作ると、次のような感じになります。

> (= {:x 2, :y 3} {:x 2, :y 3})
true

これは関数型の様式で、全てを不変の値として扱います。

関数型言語でない場合でもバリューオブジェクトを作ることはよくあります。例えばJavaでは、デフォルトの点クラスは期待通りに動作します。

assertEquals(new Point(2, 3), new Point(2, 3)); // Java

これが機能するのは、Pointクラスがデフォルトのequalsメソッドを値のテストのためにオーバーライドするからです。2 3

JavaScriptでも似た様なことができます。


class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  equals (other) {
    return this.x === other.x && this.y === other.y;
  }
}
const p1 = new Point(2,3);
const p2 = new Point(2,3);
assert(p1.equals(p2));

ここでのJavaScriptの問題点は、定義したequalsメソッドが謎に包まれていることです。


const somePoints = [new Point(2,3)];
const p = new Point(2,3);
assert.isFalse(somePoints.includes(p)); // 求めている結果ではない

//こうする必要がある
assert(somePoints.some(i => i.equals(p)));

これはJavaでは問題にはなりません。Object.equalsがJavaのコアライブラリで定義されており、全てのライブラリがObject.equalsを利用して比較しているためです。(==が利用されるのは通常プリミティブの比較のみです)

バリューオブジェクトによる優れた帰結の一つとして、メモリ上に同じオブジェクトへの参照が存在する、もしくは等価な値に対して異なる参照が存在することを気にする必要がない、ということが挙げられます。しかしながら、知らぬが仏の状態によって問題が引き起こされる可能性があります。Javaを使って説明します。


Date retirementDate = new Date(Date.parse("Tue 1 Nov 2016"));

// 退職パーティーが必要であることを意味している
Date partyDate = retirementDate;

// しかし退職日が火曜日になっている、パーティーを週末にしよう
partyDate.setDate(5);

assertEquals(new Date(Date.parse("Sat 5 Nov 2016")), retirementDate);
// あれれ、さらに3日働かなければならなくなった :-(

これはエイリアスバグの例です。ある場所で日付を変更したところ、想定外の結果になってしまいました。4 エイリアスバグを避けるために、私はシンプルですが重要なルール: バリューオブジェクトは不変であるべき に従っています。 パーティーの日付を変更したい場合は、代わりに新しいオブジェクトを生成します。

Date retirementDate = new Date(Date.parse("Tue 1 Nov 2016"));
Date partyDate = retirementDate;

// 日付を不変として扱う
partyDate = new Date(Date.parse("Sat 5 Nov 2016"));

// これでまだ火曜日に退職できる
assertEquals(new Date(Date.parse("Tue 1 Nov 2016")), retirementDate);

もちろんオブジェクトが本当に不変なのであれば、バリューオブジェクトを不変として扱う方が遥かに簡単です。 オブジェクトの場合、設定メソッドを書くことなく実現できます。初期のJavaScriptのクラスだと次の様になります。5


class Point {
  constructor(x, y) {
    this._data = {x: x, y: y};
  }
  get x() {return this._data.x;}
  get y() {return this._data.y;}
  equals (other) {
    return this.x === other.x && this.y === other.y;
  }
}

不変性が私の好みの技術である理由はエイリアスバグを回避することができるためです。一方で代入時にコピーを作成することでエイリアスバグを回避することも可能です。C#の構造体の様に、言語によってはこうした機能を提供しています。

概念を参照オブジェクトとして扱うか、バリューオブジェクトとして扱うかは文脈に依存します。 例えば、郵便物の住所は値の等しい単純な構造体として扱うのがいいでしょう。しかしながら、より洗練されたマッピングシステムにおいては、階層化モデルとして定義し、参照オブジェクト化する方が良いかもしれません。モデリングの問題は大抵の場合、文脈によって解決策が異なります。6

文字列の様な一般的なプリミティブを適切なバリューオブジェクトに置き換えることは良いアイデアです。例えば、電話番号は文字列として表現することが可能ですが、電話番号オブジェクトに置き換えることで変数やパラメータがより明確になり(言語がサポートしている場合は型チェックを行います)、自然と照合の方にフォーカスでき、不適切な動作(整数のID番号で演算するなど)を回避できます。

点、お金、範囲といった小さなオブジェクトはバリューオブジェクトの良い例です。しかし、より大きな構造体でも、概念的な同一性を持たない場合や、プログラム周りで参照を共有する必要がない場合は、バリューオブジェクトとしてプログラムすることもあります。このことは、不変性をデフォルトとする関数型言語とより自然に適合します。7

バリューオブジェクト、特に小さなオブジェクトに関しては見逃されがちで、考える価値がないほど些細なものとして見なされている様に思います。しかし一度良いバリューオブジェクトのセットを見つけてしまえば、よりリッチな振る舞いを作り込むことができます。試したい時は、まず範囲オブジェクトを利用し、開始属性と終了属性の重複操作を防ぐところから見ていきましょう。よりリッチな振る舞いになるはずです。こうしたドメイン固有のバリューオブジェクトがリファクタリングの焦点になったコードベースによって、システムが劇的に単純化することは少なくありません。

この様な単純化がサプライズになることは少なくないでしょう。何度か見かける頃には良き友人になっています。

謝辞

James Shore、Beth Andres-Beck、Pete Hodgsonが、JavaScriptでバリューオブジェクトを使用した経験を共有してくれました。

Graham Brooks、James Birnie、Jeroen Soeters、Mariano Giuffrida、Matteo Vaccari、Ricardo Cavalcanti、Steven Lowe は、メーリングリストで貴重なコメントを提供してくれました。

さらに読む

Vaughn Vernon氏の説明は、DDDの観点からバリューオブジェクトを議論したものとしては、おそらく最高のものでしょう。彼は、値とエンティティの決定方法、実装のヒント、バリューオブジェクトを永続化するためのテクニックを網羅しています。

この用語が広まり始めたのは90年代初頭のことです。その頃の話をしている2冊の本がPoEAADDDです。WardのWikiにも興味深い議論がありました。

用語の混乱の原因の1つは、今世紀に入ってからJ2EEの文献でData Transfer Objectに対して “バリューオブジェクト”が使われたことがあるためです。今ではそのような使い方はほとんどされていませんが、たまに遭遇する可能性もあります。

注釈

  1. ドメイン駆動設計では、Evansはバリューオブジェクトとエンティティを対比関係として分類しています。私はエンティティを参照オブジェクトの一般的な形態として捉えていますが、「エンティティ」という用語はドメインモデル内でのみしか使用しません。一方で参照/バリューオブジェクトの二分法は全てのコードに対して使用しています。 

  2. これは厳密には awt.Point のスーパークラスである awt.geom.Point2D で行われます。 

  3. Java におけるオブジェクトの比較はほとんどの場合等号で行われますが、これ自体が少し厄介です。というのも、等価演算子==以上にそれを利用することを覚えなければならないためです。煩わしいですが、Javaプログラマはすぐそれに慣れてしまいます。Stringが同じ様に振る舞うためです。他のOO言語ではこれを避けることができます。Rubyは ==演算子を使いますが、それをオーバーライドすることができます。 

  4. Java-8以前の日付と時刻のシステムの機能性は最悪で、たくましく競い合っています - しかし、私の一票はこれ一択です。ありがたいことに、Java8のjava.timeパッケージを使えばこうした現象はほぼ回避できます。 

  5. これは厳密には不変ではありません。クライアントが_dataプロパティを操作できるためです。しかし、規律のあるチームであれば、実際には不変にすることができます。もしチームの規律が不十分ではないかと心配になった場合は、freeze を使うかもしれません。実際、単純な JavaScript オブジェクトに freeze を使うこともできますが、私はアクセサが宣言されているクラスの明示性の方が好きです。 

  6. この点については、Evans氏のDDDの本に詳しく書かれています。 

  7. 不変性は参照オブジェクトにとっても価値があります。例えば販売注文がリクエスト取得中に変更されない場合、それを不変にすることは価値があります。しかし、もし一意の注文番号に基づいて等価性を判断しているのであれば、その販売注文はバリューオブジェクトにはできません。