Value Object
1. 目的
プリミティブ型に型を付けて何を表しているのか明確にしたい
- プリミティブ型はそれ自体は単なる数値/文字列/日付etc...であり、それが何を意味しているのかわかりにくい
- 何を意味しているかわからないコードは保守が困難である
- そのデータが何を意味しているのかを明確にすることでプログラムの保守性を向上させたい
2. 課題
プリミティブ型はドメイン知識を表現していない
- 例えば、int型の
18
は単に数値の18
でしかないが、システムが対象とするドメインの文脈では年齢を表しているかもしれないし、18
はサービス利用可能年齢を表しているかもしれない
- intの
18
だけではそれが示す文脈が全くわからない点に問題がある
- 数値に限らず、文字列や日付でも同じである
3. 対策
プリミティブ型の値をラップするオブジェクトを定義する
class Age:
def __init__(self, age):
sef._age = age
def value(self):
return sef._age
def is_over_18(self):
return sef._age >= 18
-
Age
という型を定義することでAge
が持つ数値が年齢であることが明確になった
- また、18歳以上かを判定するメソッドも付与することで判定処理がコード各所に散らばることを防ぐことができる
- このようにプリミティブ型をラップし、ドメインの知識を表現するユーザー定義型をドメイン駆動設計では
Value Object
と呼ぶ
4. メリット
値が何を表しているのかが明確になる
- プリミティブ型をクラスでラップすることで、その値が何を表現しているのか明確になる
- 型が定義されることでプログラミング言語の型チェック機能を利用することができるようになり、IDEの補完機能やコンパイラによるチェックなど生産性と保守性を向上させることができる
値の範囲を明確にできる
- クラスであるということは、当然コンストラクタでデータを受け取ることになる
- つまり、コンストラクタでデータの範囲チェックをすることが可能になる
class Age:
def __init__(self, age):
if age < 0:
raise ValueError('年齢の範囲が異常')
if age >= 130:
raise ValueError('年齢の範囲が異常')
sef._age = age
def value(self):
return sef._age
def is_over_18(self):
return sef._age >= 18
- ↑の例ではコンストラクタで値を受け取る際に値の範囲チェックをしている
- 年齢がマイナスになることは定義上ありえないし、(確認されている限りでは)130年以上生きている人間もいないのでこれらを異常データとして除外している
- 取り得る値を限定することでバグを未然に防いだり、異常値に気付きやすくするなどの効果がある
- プリミティブ型ではこのような処理はできないので、
Value Object
の明確な優位性と言える
5. デメリット
コード量が増える
- ドメインの概念に個別に対応したクラスを作ることになるので、コード量は当然増える
6. 注意
参考資料