値オブジェクト
2016年11月14日
プログラミングにおいて、私はしばしば物事を複合として表現することが有用だと感じます。2D座標はx値とy値で構成されます。金額は数値と通貨で構成されます。日付範囲は開始日と終了日で構成され、それ自体が年、月、日の複合体となることがあります。
このように作業していると、2つの複合オブジェクトが同じかどうかという疑問に突き当たります。両方とも(2,3)のデカルト座標を表す2つのポイントオブジェクトがある場合、それらを等しいと扱うのは理にかなっています。プロパティの値、この場合はx座標とy座標によって等しいオブジェクトは、値オブジェクトと呼ばれます。
しかし、プログラミング時に注意しないと、プログラムでその動作を得られない可能性があります。
JavaScriptでポイントを表現したいとしましょう。
const p1 = {x: 2, y: 3}; const p2 = {x: 2, y: 3}; assert(p1 !== p2); // NOT what I want
残念ながら、このテストは合格します。これは、JavaScriptがjsオブジェクトの等価性をテストする際に、その内容の値を無視して参照を調べるためです。
多くの場合、値ではなく参照を使用する方が理にかなっています。大量の販売注文をロードして操作する場合、各注文を単一の場所にロードするのが理にかなっています。その後、アリスの最新の注文が次の配達にあるかどうかを確認する必要がある場合、アリスの注文のメモリ参照、つまりIDを取得し、その参照が配達の注文リストにあるかどうかを確認できます。このテストでは、注文の内容を気にする必要はありません。同様に、一意の注文番号に依存して、アリスの注文番号が配達リストにあるかどうかをテストする場合があります。
したがって、私はオブジェクトを区別する方法に応じて、値オブジェクトと参照オブジェクトの2つのクラスのオブジェクトとして考えると便利だと感じます[1]。各オブジェクトがどのように等価性を処理することを期待するかを理解し、私の期待どおりに動作するようにプログラムする必要があります。その方法は、作業しているプログラミング言語によって異なります。
一部の言語では、すべての複合データが値として扱われます。Clojureで単純な複合体を作成すると、次のようになります。
> (= {:x 2, :y 3} {:x 2, :y 3}) true
これは関数型スタイルであり、すべてを不変の値として扱います。
しかし、関数型言語を使用していない場合でも、値オブジェクトを作成できることがよくあります。たとえば、Javaでは、デフォルトのポイントクラスは私が期待する動作をします。
assertEquals(new Point(2, 3), new Point(2, 3)); // Java
これが機能するのは、ポイントクラスがデフォルトの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メソッドが他のJavaScriptライブラリにとって謎であるということです。
const somePoints = [new Point(2,3)]; const p = new Point(2,3); assert.isFalse(somePoints.includes(p)); // not what I want //so I have to do this assert(somePoints.some(i => i.equals(p)));
Javaでは、Object.equals
がコアライブラリで定義されており、他のすべてのライブラリが比較に使用するため、これは問題になりません(==
は通常プリミティブにのみ使用されます)。
値オブジェクトの良い結果の1つは、メモリ内の同じオブジェクトへの参照を持っているか、等しい値を持つ別の参照を持っているかを気にする必要がないことです。ただし、注意しないと、その幸せな無知が問題を引き起こす可能性があります。これは、Javaの例で説明します。
Date retirementDate = new Date(Date.parse("Tue 1 Nov 2016")); // this means we need a retirement party Date partyDate = retirementDate; // but that date is a Tuesday, let's party on the weekend partyDate.setDate(5); assertEquals(new Date(Date.parse("Sat 5 Nov 2016")), retirementDate); // oops, now I have to work three more days :-(
これはエイリアシングバグの例です。ある場所で日付を変更すると、予想以上の影響があります[4]。エイリアシングバグを回避するために、私はシンプルだが重要なルールに従います。値オブジェクトは不変である必要があります。パーティーの日付を変更したい場合は、代わりに新しいオブジェクトを作成します。
Date retirementDate = new Date(Date.parse("Tue 1 Nov 2016")); Date partyDate = retirementDate; // treat date as immutable partyDate = new Date(Date.parse("Sat 5 Nov 2016")); // and I still retire on Tuesday 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番号での算術演算など)を回避できます。
点、金額、範囲などの小さなオブジェクトは、値オブジェクトの良い例です。ただし、より大きな構造も、概念的なIDがない場合や、プログラム全体で参照を共有する必要がない場合は、値オブジェクトとしてプログラムできることがよくあります。これは、デフォルトで不変性を使用する関数型言語とより自然に適合します。[7]
私は、特に小さな値オブジェクトが見過ごされがちであり、考える価値がないほど些細なものと見なされがちであることに気づきます。しかし、適切な値オブジェクトのセットを見つけると、それらに対して豊富な動作を作成できることがわかります。これを試すには、Rangeクラスを使用し、より豊富な動作を使用することで、開始属性と終了属性の重複した操作をどのように防ぐかを確認してください。私は、このようなドメイン固有の値オブジェクトがリファクタリングの焦点となり、システムの大幅な簡素化につながるコードベースに遭遇することがよくあります。このような簡素化は、何度か目にするまでは人々を驚かせますが、それまでには良い友人となっています。
謝辞
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の観点から見た値オブジェクトの最も詳細な議論です。彼は、値とエンティティのどちらを選択するか、実装のヒント、値オブジェクトを永続化するための手法について説明しています。
この用語は、2000年代初頭に普及し始めました。その頃から値オブジェクトについて説明している2冊の本は、PoEAAとDDDです。Ward's Wikiでもいくつかの興味深い議論がありました。
用語の混乱の原因の1つは、20世紀末頃に一部のJ2EE文献で「値オブジェクト」がデータ転送オブジェクトに使用されていたことです。その使用法は現在ほとんど消えていますが、遭遇する可能性があります。
注釈
1: ドメイン駆動設計では、Evansの分類で値オブジェクトとエンティティを対比させています。私はエンティティを参照オブジェクトの一般的な形式と見なしていますが、「エンティティ」という用語はドメインモデル内でのみ使用し、参照/値オブジェクトの二分法はすべてのコードで役立ちます。
2: 厳密には、これはawt.Pointのスーパー クラスであるawt.geom.Point2Dで行われます
3: Javaでのほとんどのオブジェクト比較はequals
で行われます。これは、等価演算子==
ではなくそれを使用することを覚えておく必要があるため、少し面倒です。これは面倒ですが、JavaプログラマーはStringが同じように動作するため、すぐに慣れます。他のOO言語ではこれを回避できます。Rubyは==
演算子を使用しますが、オーバーライドが可能です。
4: Java 8以前の日付と時刻システムの最悪の機能には熾烈な競争がありますが、私の票はこれです。ありがたいことに、Java 8のjava.time
パッケージを使用すれば、このほとんどを回避できます
5: これは、クライアントが_data
プロパティを操作できるため、厳密には不変ではありません。ただし、十分に規律のあるチームであれば、実際には不変にすることができます。チームが十分に規律がないことを懸念している場合は、freeze
を使用する可能性があります。実際、単純なJavaScriptオブジェクトにfreezeを使用することもできますが、宣言されたアクセサーを持つクラスの明示性が好みです。
6: この詳細については、EvansのDDD本で詳しく説明されています。
7: 不変性は参照オブジェクトにも役立ちます。たとえば、販売注文がgetリクエスト中に変更されない場合、それを不変にすることは価値があります。これは、便利であればコピーしても安全です。ただし、一意の注文番号に基づいて等価性を判断する場合、これは販売注文を値オブジェクトにするものではありません。