ドメイン駆動設計について調べていると、データベースのカラム全部にValueObjectを作っちゃった話とか、値を入れるだけでメソッドが一つもないValueObjectを作っちゃった話とかたまに見かけます。
こういうのは、ValueObjectの「不変で交換可能で値として等価で〜」という定義に意識がむきすぎちゃった結果なのかなと思います。
そういう定義はわきに置いて、「メソッドがあると便利な値ってないかな?」という発想の方が本当に必要なValueObjectを見つけられるんじゃないかな、というのがこのエントリで言いたいことです。
ValueObjectのパターン
本題に入る前に、どんなメソッドがあると便利かわかっているとValueObjectを見つけやすいので、そちらについて話します。
私が見つけているValueObjectのパターンは4つあります。
- ラッパーValueObject
- 計算可能なValueObject
- センシティブなValueObject
- インセンシティブ(鈍感)なValueObject
ラッパーValueObject
コード値などをラップして判定メソッドを作ります。
HTTPステータスコードを題材にしてValueObjectにしたコード例12。
class HttpStatusCodeValueObject
attr_accessor :status_code
def initialize(status_code)
self.status_code = status_code
end
# 2xx Success 成功 のステータスコードか
def success?
200 <= status_code && status_code < 300
end
end
RubyやRailsだと標準的な型を拡張する形でよくやっていることだと思います。value % 2 == 1
をvalue.odd?
のように書けたり。
計算可能なValueObject
ECサイトを作っているとして、配達予定日の計算は注文日 + 出荷までの作業時間 + 倉庫から顧客までの配達時間
になります。でも、「出荷までの作業時間」も「倉庫から顧客までの配達時間」も幅があるので、こんな感じのコードになります。
order_date = Date.parse('2020-01-01')
work_days = 0..1 # 出荷までの作業時間は1日以内
delivery_days = 1..2 # 倉庫から顧客までの配達時間は1日から2日
# こう書けるとうれしいけど、エラーになってしまう
# 注文日 + 出荷までの作業時間 + 倉庫から顧客までの配達時間
# order_date + work_days + delivery_days
# きびしいげんじつ
delivery_date_from = order_date
delivery_date_to = order_date
delivery_date_from += work_days.first
delivery_date_to += work_days.last
delivery_date_from += delivery_days.first
delivery_date_to += delivery_days.last
なので、日付の範囲を計算できるクラスをValueObjectとして作ります。
class DateRangeValueObject
attr_accessor :from, :to
def initialize(from, to = nil)
self.from = from
self.to = to || from
end
# 他の演算子は不要なので加法のみ定義
def +(other)
other_from = other
other_to = other
if other.is_a?(Range)
other_from = other.first
other_to = other.last
end
self.class.new(from + other_from, to + other_to)
end
# プリミティブな値にエクスポートする
def to_range
from..to
end
end
このクラスによって、計算が簡単に書けるようになりました。
value_object = DateRangeValueObject.new(order_date)
value_object += work_days
value_object += delivery_days
value_object.to_range
これもRubyやRailsだと標準的な型を拡張する形でよくやっていることだと思います。
センシティブなValueObjectとインセンシティブ(鈍感)なValueObject
値チェックしておかしな値が入ってきたら例外を投げるというのはよくあることです。例外を投げるまではいかなくとも、失敗の結果を返せばいい場合もあります。
例外を投げる方は、お金の計算で存在しない通貨を指定された場合などがそうで、Moneyパターン3を作るならコンストラクタで不正な通貨だったら例外を投げるように作ると思います。
こういうValueObjectをセンシティブなValueObjectと私は呼んでいます。
失敗の結果を返せばいいケースは、ラッパーValueObjectで想定していない値が指定された場合がそうです。ラッパーValueObjectのコード例でHttpStatusCodeValueObject
クラスを書きましたが、status_code
に-1
を指定されても問題なく動きます。これは浮動少数点計算のNaN
やRDBにおけるNULL
に対する演算に近い振る舞いです。正当な値が指定されているかチェックするvalid?
メソッドを作れば、ユーザー入力のバリデーションにも利用できます。
こういうValueObjectをインセンシティブ(鈍感)なValueObjectと私は呼んでいます。
不正な値が入ってきたらすぐ反応するからセンシティブ、valid?
が呼ばれるまで気づかないからインセンシティブ(鈍感)というわけです。
「メソッドがあると便利な値ってないかな?」を考える
必要な物を探すテクニックで、とりあえず全部机の上にならべてから要らないものを取り除いていくという方法があります。とりこぼしがなく終わらせどきがわかりやすい方法です。
ValueObjectを見つけるときにもこの方法は使えます。
まずは取り組んでいるユーザーストーリーや画面を見て、使われているデータをメモ帳に書き出してみます。
その中から、入力するだけ、出力するだけの値を取り除いていきます。
- ユーザー名や住所のような単純な文字列とか
- 希望年収や募集人数のような単純な数値など
ユーザー名が必須入力だとしても、センシティブなValueObjectを作らずフレームワークのバリデーション処理に任せた方が良いケースもあります。
電話番号のような複雑なケースでもカスタムバリデータを使えば良いかもしれません。
残った値に対して質問を投げかけてみます。
- その値5を使うところで定型文になる処理ってないかな?
- その値を使うための処理というより、その値のものといえる処理ってないかな?
- その値から直接呼べると便利な処理ってないかな?
いずれかでイエスならValueObjectをつくりましょう。
そのうえで、ValueObjectとしての性質は必要なら作り込むという方法でよいと思います。
補足:静的型付け言語なら、ハンガリアン記法(アプリケーションハンガリアン)を型で実現するためにメソッドなしのValueObjectを作っても良いのかなと考えてます。
このエントリは要求分析駆動設計のデータディクショナリを再編し汎用的な内容に書き換えたものです。
めっちゃ長いけど、RailsでDDDっぽいことする話です。