LoginSignup
1
2

More than 3 years have passed since last update.

メソッドがあると便利な値を探してValueObjectにしよう

Posted at

ドメイン駆動設計について調べていると、データベースのカラム全部にValueObjectを作っちゃった話とか、値を入れるだけでメソッドが一つもないValueObjectを作っちゃった話とかたまに見かけます。
こういうのは、ValueObjectの「不変で交換可能で値として等価で〜」という定義に意識がむきすぎちゃった結果なのかなと思います。
そういう定義はわきに置いて、「メソッドがあると便利な値ってないかな?」という発想の方が本当に必要なValueObjectを見つけられるんじゃないかな、というのがこのエントリで言いたいことです。

ValueObjectのパターン

本題に入る前に、どんなメソッドがあると便利かわかっているとValueObjectを見つけやすいので、そちらについて話します。
私が見つけているValueObjectのパターンは4つあります。

  • ラッパーValueObject
  • 計算可能なValueObject
  • センシティブなValueObject
  • インセンシティブ(鈍感)なValueObject

ラッパーValueObject

コード値などをラップして判定メソッドを作ります。

HTTPステータスコードを題材にしてValueObjectにしたコード例12

http_status_code_value_object.rb
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 == 1value.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として作ります。

date_range_value_object.rb
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に当たると思います。4

失敗の結果を返せばいいケースは、ラッパーValueObjectで想定していない値が指定された場合がそうです。ラッパーValueObjectのコード例でHttpStatusCodeValueObjectクラスを書きましたが、status_code-1を指定されても問題なく動きます。これは浮動少数点計算のNaNやRDBにおけるNULLに対する演算に近い振る舞いです。正当な値が指定されているかチェックするvalid?メソッドを作れば、ユーザー入力のバリデーションにも利用できます。
こういうValueObjectをインセンシティブ(鈍感)なValueObjectと私は呼んでいます。
不正な値が入ってきたらすぐ反応するからセンシティブ、valid?が呼ばれるまで気づかないからインセンシティブ(鈍感)というわけです。

「メソッドがあると便利な値ってないかな?」を考える

必要な物を探すテクニックで、とりあえず全部机の上にならべてから要らないものを取り除いていくという方法があります。とりこぼしがなく終わらせどきがわかりやすい方法です。
ValueObjectを見つけるときにもこの方法は使えます。

まずは取り組んでいるユーザーストーリーや画面を見て、使われているデータをメモ帳に書き出してみます。
その中から、入力するだけ、出力するだけの値を取り除いていきます。

  • ユーザー名や住所のような単純な文字列とか
  • 希望年収や募集人数のような単純な数値など

ユーザー名が必須入力だとしても、センシティブなValueObjectを作らずフレームワークのバリデーション処理に任せた方が良いケースもあります。
電話番号のような複雑なケースでもカスタムバリデータを使えば良いかもしれません。

残った値に対して質問を投げかけてみます。

  • その値5を使うところで定型文になる処理ってないかな?
  • その値を使うための処理というより、その値のものといえる処理ってないかな?
  • その値から直接呼べると便利な処理ってないかな?

いずれかでイエスならValueObjectをつくりましょう。
そのうえで、ValueObjectとしての性質は必要なら作り込むという方法でよいと思います。

補足:静的型付け言語なら、ハンガリアン記法(アプリケーションハンガリアン)を型で実現するためにメソッドなしのValueObjectを作っても良いのかなと考えてます。


このエントリは要求分析駆動設計データディクショナリを再編し汎用的な内容に書き換えたものです。
めっちゃ長いけど、RailsでDDDっぽいことする話です。


  1. サンプルコードなので雑なステータスコード判定にしています 

  2. attr_accessorを使ってますが、不変性が損なわれることが気になるならprivateにするかインスタンス変数参照する形に変更すればOKです 

  3. お金のValueObjectとして有名なパターン 

  4. ちゃんと読み切っていないので、違っていたらすみません・・・ 

  5. 1つの値だけではなく、複数の値をセットで使う場合もあります 

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2