バリューオブジェクトについての追記
昨今(2022年8月ごろ)、バリューオブジェクトに関してtwitterで色々話題になっている。そうした中、私もバリューオブジェクト(ValueObject)パターン(PofEAA)について記述している以上、何かしら言及しておく必要があるだろうと考え追記している。
定義
PofEAAから引用すると以下の通りである。
IDに基づいた等価性を確保していない、MoneyやDate Rangeなどのシンプルな小型オブジェクト。
(中略)
バリューオブジェクトの等価性の概念はクラス内のフィールド値に基づいている。2つのDateオブジェクトで日、月、年の値が同じ場合は、等価ということになる。
(中略)
バリューオブジェクトが機能するためには、バリューオブジェクトを不変、つまり作成時のフィールドがそのまま変更できないようにすることが有効である。
エンタープライズアプリケーションアーキテクチャパターン 18.6 バリューオブジェクト
また、バリューオブジェクトの具体例としてマネーパターンを挙げ、amount
とcurrency
フィールド、+
、-
、*
、allocate
、>
、=
メソッドを持つクラスを例示している。
エリック・エヴァンスのドメイン駆動設計においても似たようなもので、一部の例外を除いて不変であることを強く推奨している。
なお、私の理解はPofEAAとエヴァンス本のバリューオブジェクトは同じ物を指しており、エリック・エヴァンスはドメインモデルのうちバリューオブジェクトパターンを適用できるものがあると主張しているという理解である。
日本のDDD界隈で言われている定義
すべてを追っているわけではないし、はっきり言ってしまえば、私の印象で彼らが主張しているように思う定義を列挙する。
- 値をクラスで表現するパターン
- プリミティブ型ではなく固有型を作ることでタイプセーフになる
- コンストラクタでフィールド値をチェックすることで不正な値が入らないようになる
- あらゆる値をバリューオブジェクトにすることで安全になる
- バリューオブジェクトはドメインオブジェクトである
- 上記のいずれかを論拠として可読性やメンセナンス性が上がる
各論に関する私の所感
値をクラスで表現するパターン
間違ってはいないが、フィールド値によって等価性を判断するということに触れないと片手落ちのように思う。
プリミティブ型ではなく固有型を作ることでタイプセーフになる
これは、cm単位の数値を渡すべきところでinch単位の数値を渡してしまうようなミスを防げる、つまり、十数年前にアプリケーションハンガリアン記法で解決していたことを型で解決できるということと理解している。
論として正しいが、バリューオブジェクトの特徴として説明するのは不適切だと感じる。動的型付け言語でもバリューオブジェクトは使えるが、その場合はこの性質を持たない。
静的型付け言語において、バリューオブジェクトを使うとそういうメリットが生まれることがあるというのが正確な説明になると思う。
ただまあ、マネーパターンのように単位を持つようにすれば、cm単位とinch単位の取り違えそのものがなくなるし、タイプセーフで防げる値の取り違えというのがどの程度起きるのかという気はしている。
少なくとも、このメリットがあるからバリューオブジェクトクラスを考えなしに量産しようとするのは間違いだろうと思う。
コンストラクタでフィールド値をチェックすることで不正な値が入らないようになる
そういうこともできるが、こうするのがバリューオブジェクトであるという説明は明らかに間違いである。
不正な値が入り込む余地がないレイヤ(上位レイヤでバリデーション済など)でバリューオブジェクトを使うのであれば、そういうチェックをする必要性はない。
確かにそうすることで安全にはなるが、悪戯に実装コストとインスタンス生成コストを上げることになる。
あらゆる値をバリューオブジェクトにすることで安全になる
安全にはなるかもしれないが、あらゆる値をバリューオブジェクトにせよという方針は、普通のプロジェクトなら明らかに間違った方針である。
確かにそうすることで安全にはなるかもしれないが、悪戯に実装コストとインスタンス生成コストを上げることになる。
バリューオブジェクトはドメインオブジェクトである
PofEAAのようにDDDの範囲外でバリューオブジェクトを定義している以上、明確な間違いである。
マネーパターンについても同様である。
ドメイン知識をもたない(ドメインオブジェクトでない)バリューオブジェクトは存在しうるし、逆にドメイン知識を持つ(ドメインオブジェクトである)バリューオブジェクトも存在しうる。
一部の論について
- コンストラクタでフィールド値をチェックすることで不正な値が入らないようになる
- あらゆる値をバリューオブジェクトにすることで安全になる
これら二つについては、セキュア・バイ・デザインという本ででてくるDomain Primitiveというパターン(?)のことで、バリューオブジェクトのことでないということが指摘されている。
私は当該の本を読んでいないので正確なところは知らないが、書籍名の通りセキュアにする強い要求があるなら正しいが、通常のプロジェクトにおいては異常な品質意識であり、実装コストを上げ、メンテナンス性、可読性を下げると感じる。
私のスタンス
私のバリューオブジェクトに対する理解というかスタンスは
- バリューオブジェクトはIDやメモリ上のアドレスなどによらず、各フィールドの値が一致しているかどうかで等価かどうか判断する
==
メソッドやequals
メソッドの実装は任意である- プロジェクト全体としてそのバリューオブジェクト同士を等価比較することがないなら、使わないメソッドなのだから省略しても良いというだけの話
==
メソッドやequals
メソッド以外のメソッドも持てる- そのメソッドがドメインロジックであっても、なくても良い
- ドメインロジックを持つならドメインモデルのバリューオブジェクト
- ドメインロジックを持たないなら、ただのバリューオブジェクト
1 + 1.1
や3.odd?
のようなRubyにおけるプリミティブ型と同じように振る舞うようにするべきである- 不変であることと、新しいインスタンスを生成すること、値を使ったなんらかの処理をするメソッドを任意個数持つ
- コンストラクタでのチェックは必要ならやる
- 通常はやらない
- 作成するかどうか
- 複数の値の組み合わせで等価かどうかチェックしたいオブジェクトがあるなら作る
- 単一の値であってもメソッドを持たせることが有用なら作る
- (私はRubyプログラマだが)タイプセーフにするメリットが作るコストを上回る場合は作る
- それ以外はドメインオブジェクトとしてモデリングされていても作らない
- プリミティブ型のまま取り扱う
という感じである。原義にプラスしてRubyでのプリミティブ型のような挙動にするべきといっているが、原義から類推できる性質だと私は考えている。