【ドメイン駆動設計】ValueObjectサンプルコード：プリミティブなオブジェクトみたいに振る舞う

## はじめに

DDD（ドメイン駆動開発）に登場するValueObjectをRubyで実装しようとして
コードを書き進めると、プリミティブっぽく振る舞うようにするにはどうすれば良いんだっけ？
と悩むシーンがあったので
どんなメソッドを実装しておくと便利になるのか、サンプルにまとめてみました。

（ValueObject自体の説明はここでは割愛させていただきます）

## ValueObjectの特徴

ValueObjectを実現するには、次の特徴を実装しなければなりません。

- 値は不変（後から変更できない）
- 値が同じなら同一オブジェクトと判断

（単一の値よりも複数の値をひとかたまり(合成物)として扱う方がより効果的）

他にもIntegerやStringなどのプリミティブなオブジェクトと同じような特徴を備えていると便利です。

- 比較演算子で比較できる
- 配列に入れてソートできる
- ハッシュのキーにできる
- 範囲（Range）に使える

## サンプル：年月をValueObjectで表現してみる

年月をValueObjectとして表現します。

- 年と月を渡すとオブジェクトが作られる
- 月初と月末の日付をもっている
- ある日付を渡すとその年月に含まれているか判断することができる

## サンプルコード（ruby）

```ruby
require 'date'

class YearMonthValue
  include Comparable

  def initialize(year, month)
    min = Date.new(year, month, 1)
    max = Date.new(year, month, (min.next_month - 1).day)
    @range = Range.new(min, max)
  end

  def value
    @range.min
  end

  def cover?(date)
    @range.cover?(date)
  end

  def to_s
    value.strftime('%Y/%m')
  end

  def <=>(other)
    value <=> other.value
  end

  def eql?(other)
    value.eql?(other.value)
  end

  def hash
    value.hash
  end

  def succ
    date = @range.min.next_month
    self.class.new(date.year, date.month)
  end

end
```

## サンプルコード各メソッド説明

### initializeメソッド

ValueObjectは値が不変になるようにしたいので、
initialize時に受け取った値をインスタンス変数に保持するようにします。
インスタンス変数を外部から変更できるようなaccessorや、
外部からインスタンス変数が変更されるようなメソッドは作らないようにします。

```ruby
def initialize(year, month)
  min = Date.new(year, month, 1)
  max = Date.new(year, month, (min.next_month - 1).day)
  @range = Range.new(min, max)
end
```

### valueメソッド

このオブジェクトが持つ値を返すようにします。後で出てくる比較などに使います。
このサンプルではRangeオブジェクトのminを返していますが、to_sや他の値を返すやり方もあります。

```ruby
def value
  @range.min
end
```

### cover?(date)メソッド

この年月クラスは、ある日付を渡すとその年月に含まれているか判断できるようにします。
これはValueObjectの特徴を実現するためではなく、年月クラスとして便利に使うためのものです。

```ruby
def cover?(date)
  @range.cover?(date)
end
```


```ruby:利用例
ym = YearMonthValue.new(2022, 7)
puts ym.cover?(Date.new(2022, 7, 15))
true
puts ym.cover?(Date.new(2022, 8, 15))
false
```

### to_sメソッド

このオブジェクトが持つ値の文字列表現を返します。

```ruby
def to_s
  value.strftime('%Y/%m')
end
```

```ruby:利用例
puts YearMonthValue.new(2022, 7).to_s
2022/07
puts YearMonthValue.new(2022, 9).to_s
2022/09
```



### <=>(other)メソッド

クラスは”Comparable”をIncludeしています。
そして、この<=>メソッドを実装することでValueObject同士を比較することができるようなります。

==、<、>、<=、=>、!= などの比較演算子は、true または false を返しますが

<=>演算子は、true、falseでなく
等しければ0、左辺が大きければ+1、右辺が大きければ-1 をそれぞれ返します。

このメソッドの結果を使ってComparableの処理が行われます。

メソッドを実装していないと。。。

```ruby:実装前
puts YearMonthValue.new(2022, 7) == YearMonthValue.new(2022, 7)
false
```

同値だけれどオブジェクトが別と判定されて false となる

メソッドを実装する
（class定義の下で、include Comparableを忘れずに）

```ruby
def <=>(other)
  value <=> other.value
end
```

```ruby:実装後
puts YearMonthValue.new(2022, 7) == YearMonthValue.new(2022, 7)
true
```

オブジェクトが同一かどうかの比較（デフォルトの比較）から、
値が同じかどうかの比較に変わったので true と判定されるようになる。

そして

```ruby:利用例
puts YearMonthValue.new(2022, 7) == YearMonthValue.new(2022, 7)
true
puts YearMonthValue.new(2022, 7) != YearMonthValue.new(2022, 8)
true
puts YearMonthValue.new(2022, 8) > YearMonthValue.new(2022, 7)
true
puts YearMonthValue.new(2022, 7) < YearMonthValue.new(2022, 8)
true
```

Comparableとして機能するようになる。

さらに

```ruby:利用例
array = []
array.push(YearMonthValue.new(2022, 5))
array.push(YearMonthValue.new(2022, 7))
array.push(YearMonthValue.new(2022, 6))
array.push(YearMonthValue.new(2022, 4))
puts array.sort
2022/04
2022/05
2022/06
2022/07
```

Comparableが持つsortも使えるようになる。

このサンプルでは比較する際に、valueメソッドがあれば比較が行えるようになっていますが
必要に応じて同じクラスかどうかのチェックを行なっても良いでしょう。


### eql?(other) と hash メソッド
ハッシュのキーとして使えるようにするために必要です。


```ruby:実装前
hash = {}
hash[YearMonthValue.new(2022, 7)] = 123
hash[YearMonthValue.new(2022, 8)] = 456
puts hash[YearMonthValue.new(2022, 7)]
puts hash[YearMonthValue.new(2022, 8)]
（何も表示されない）
```

ハッシュに値を代入する際のキーとして指定した「YearMonthValue.new(2022, 7)」と
ハッシュから値を取り出す際にキーとして指定した「YearMonthValue.new(2022, 7)」が
同一ではないと判断されてハッシュから値を取り出すことが出来ていない。
「YearMonthValue.new(2022, 8)」も同様。


```ruby
def eql?(other)
  value.eql?(other.value)
end

def hash
  value.hash
end
```


```ruby:実装後
hash = {}
hash[YearMonthValue.new(2022, 7)] = 123
hash[YearMonthValue.new(2022, 8)] = 456
puts hash[YearMonthValue.new(2022, 7)]
puts hash[YearMonthValue.new(2022, 8)]
123
456
```

ハッシュに値を代入する際のキーとして指定した「YearMonthValue.new(2022, 7)」と
ハッシュから値を取り出す際にキーとして指定した「YearMonthValue.new(2022, 7)」が
同一と判断されてハッシュから値を取り出すことが出来た。
「YearMonthValue.new(2022, 8)」も同様。

### succメソッド

自分が持つ値を１つ進めた新しいオブジェクトを返すメソッドです。
このメソッドを実装することでRangeオブジェクトの開始、終了に使うことができるようになります。
自分自身の値を変更せずに新しいオブジェクトを返すのがミソです。


```ruby:実装前
range = Range.new(YearMonthValue.new(2022, 1), YearMonthValue.new(2022, 5))
range.each do |ym|
  puts ym.to_s
end
（エラーが発生する）
```

```ruby
def succ
  date = @range.min.next_month
  self.class.new(date.year, date.month)
end

def <=>(other)
  value <=> other.value
end
```

```ruby:実装後
range = Range.new(YearMonthValue.new(2022, 1), YearMonthValue.new(2022, 5))
range.each do |ym|
  puts ym.to_s
end
2022/01
2022/02
2022/03
2022/04
2022/05
```

## 最後に

DDDの概念は難しく、特にRuby on Railsに導入するのはハードルが高いと感じています。
しかし、**ValueObjectは比較的シンプルな考え方**なので、
他のDDDの概念とは切り離して単独で導入することもできると考えています。
その場合は、DDDというよりもデザインパターンのImmutableオブジェクトになるのかもしれませんね。

上手く使ってコードの可読性と保守性を上げていきましょう。

## 参考
[ValueObjectという考え方](https://qiita.com/kichion/items/151c6747f2f1a14305cc)
[Rubyリファレンスマニュアル Objectクラス](https://docs.ruby-lang.org/ja/latest/class/Object.html)
[Rubyリファレンスマニュアル Hashクラス](https://docs.ruby-lang.org/ja/latest/class/Hash.html)
[Rubyリファレンスマニュアル Rangeクラス](https://docs.ruby-lang.org/ja/latest/class/Range.html)