22
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【ドメイン駆動設計】なぜ値オブジェクトそのものを比較できるようにしなければならないのか?

Last updated at Posted at 2022-07-30

値オブジェクトは以下の3つの要素を持ったオブジェクトだとされている。

1)不変である(一度インスタンスが作られたら、それが保有する属性の値は変化してはいけない)
2)交換が可能である(再代入=交換によってのみ値を変更することができる)
3)等価性によって比較される

このうち、3)は絶対に必要なんだろうか?

例えば、Date型にかなり近い、YearMonth(年と月をもったオブジェクト)を定義してみよう。

class YearMonth
  attr_reader :date

  def initialize(year, month)
    @date = Date.new(year, month, 1)
  end

end

一応値オブジェクトとして定義しているものの、「日付が1日のDate型オブジェクト」」としても外部から認識できる。なら、YearMonthそのものを比較できなくても、保有しているDate型(プリミティブ型)同士を比較すればいいんじゃないか?という疑問が生まれる。

実際にやってみよう。


year_month1 = YearMonth.new(2022, 7)
year_month2 = YearMonth.new(2022, 8)

# 等価性比較
year_month1.date == year_month2.date
# →false

パット見問題なさそうに見える。が、

0.value == 1.value

としているような気持ち悪さはある。両方Integer Objectなんだからシンプルに0 == 1でええやん。それと全く一緒の考えから、

year_month1 == year_month2
year_month1 > year_month2
year_month1 < year_month2

で比較したらいい。

そして、もう一つ見逃してはならないこのルールの意図として、変更に柔軟であることが挙げられる。

例えば、この値オブジェクトにインスタンスが一つ追加されたことを考えてみる。

class YearMonth
  attr_reader :date, :something

  def initialize(year, month, arg)
    @date = Date.new(year, month, 1)
	@something = Something.new(arg)
  end

end

year_month1 = YearMonth.new(2022, 7, arg1)
year_month2 = YearMonth.new(2022, 8, arg2)

# 等価性比較
year_month1.date == year_month2.date &&
year_month1.something == year_month1.something

この比較が一箇所ならいいが、等価性比較をしているすべての箇所で&& year_month1.something == year_month1.something を追加しなければならない。

ということで、値オブジェクトを導入する際はオブジェクトそのものの等価性比較をできるようにすることがmustであると言える。

class YearMonth
	include Comparable

    def initialize(year, month, arg)
        @date = Date.new(year, month, 1)
        @something = Something.new(arg)
    end

	# 外部から呼び出す値
	def value
		@date.strftime('%Y.%m')
	end

	# 等価性比較のために必要なメソッド
	def <=>(other)
        @date <=> other.to_date # to_dateというメソッドを持つオブジェクトなら比較できると定義
    end

	# 同じYearMonthを比較できるように自身にもto_dateメソッドを実装しておく。
    def to_date
        @date
    end
end

今回の例だと、なにか変更があったとしてもdef <=>(other) を変更するだけで、YearMonth同士の比較を実際にしているところのコードは全くいじらなくていい。

DDD全体の良さの一つは「拡張性が高い」ことだが、値オブジェクトでも等価性比較ができるようにしておくことでこのオブジェクト自身が拡張性の高い状態になる。

22
22
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
22
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?