LoginSignup
27
24

More than 5 years have passed since last update.

バリューオブジェクトの私的まとめ

Posted at

まえがき

前回しれっとバリューオブジェクトなる単語を使ったのですが、ここで一度意味を整理しておきたいと思ったのでまとめます。

バリューオブジェクトとは何か

文字通り、バリューつまり値を表現するためのオブジェクトをバリューオブジェクトと呼びます。
でもこれだけだと、問題が「値とは何か」に置き換わるだけですね。では、アプリケーション内で「値」が持つべき特性を考えてみましょう。

  1. 同じ値は等しいものとして扱われる
  2. 異なる値同士は何らかの方法で比較できる
  3. 異なる値は異なるものとして扱われる(イミュータブル)

以下では、具体例としてMoneyクラスを使います。このクラスは金額と通貨単位を持っています。下に一番素朴な実装を示します。

money.rb
# 素朴な実装
class Money
  attr_accessor :amount, :currency
  def initialize(amount, currency)
    @amount, @currency = amount, currency
  end
end

同じ値は等しい

1は当たり前といえばあまりに当たり前なことを言っています。Moneyクラスでは、金額と通貨単位が等しいとき、そのオブジェクトたちは等しいと言っていいでしょう。
しかし、先ほどの素朴な実装では、==が実装されていないため、異なるインスタンス同士は常に異なっていると判定されてしまいます。

irb
m1 = Money.new(100, 'USD')
m2 = Money.new(100, 'USD')
m1 == m2 # => false

そこで、==を実装します。

money.rb
Class Money
  def ==(other_money)
    self.amount == other_money.amount && self.currency == other_money.currency
  end
end

これで同じ値は等しくなりました。

異なる値を比較する

またもや当たり前なことですが、異なる値同士は比較できてほしいものです。Moneyの例で言えば、異なる通貨での大小比較ができると最高ですね。もしMoneyを単なる文字列や数値として実装した場合、異なる通貨での大小比較には別クラスを用意する必要があるでしょう。実際には、その比較は貨幣と金額に密接に関連した計算ですので、バリューオブジェクトを使ったほうが凝集度が高くなります。
比較をするということですので、Comparableモジュールをincludeして、必要なメソッドを実装します。

money.rb
class Money
  include Comparable
  EXCHANGE_RATES = {'USD' => {'USD' => 1, 'JPY' => 103}, 'JPY' => {'USD' => 0.0097, 'JPY' => 1}}
  def <=>(other_money)
    return nil unless other_money.is_a?(Money)
    if self.currency == other_money.currency
      self.amount <=> other_money.amount
    else
      self.amount <=> other_money.exchange_to(@currency).amount
    end
  end

  def exchange_to(currency)
    rate = EXCHANGE_RATES[@currency][currency].to_f
    Money.new(@amount * rate, currency)
  end
end

EXCHANGE_RATESの部分には実際の為替レートを使うことを想定しています。

イミュータブル

ここで、exchange_toに注目してください。自身のamountを変更する代わりに、新しいオブジェクトを生成して返しています。では、もしexchange_toが自身のamountを変更する場合、何が起こるでしょうか。

money.rb
# まずい実装
def exchange_to(currency)
  rate = EXCHANGE_RATES[@currency][currency].to_f
  self.amount = self.amount * rate
  self.currency = currency
end
irb
money1 = Money.new(1000, 'USD')
money2 = Money.new(1000, 'USD')
money1.amount == money2.amount # => true
money2.exchange_to('JPN')
money1.amount == money2.amount # => false

1つのコード内で、同じ比較式の返り値が変わってしまっています。しかし、「同じ1000ドル同士を比較した結果が変わる」というのは妙な話です。そもそも、1000ドルを円に両替したら、それはもはや別物として扱うべきでしょう。
この性質は、例えばリンゴとは決定的に違います。リンゴの場合、時間が経って腐ってしまうなどして属性が変わっても同じリンゴと言えますが、バリューオブジェクトの場合、属性が変わればもはや同じオブジェクトではありません。この性質が イミュータブルであるということです。この性質を実装するには、自身の属性を変えるようなメソッドでは常に新しいインスタンスを返すようにします。
また、amount=メソッドがあってはマズイので、アクセサをゲッターのみにしましょう。

money.rb
class Money
  attr_reader :amount, :currency
end

いつ使うのか

さてでは、バリューオブジェクトはどんなときに使うべきなのでしょうか。
1. 複数の値がまとまって一つの意味ある実体をなしているとき
2. 複雑な比較が必要なとき

1の実例は座標などがあるでしょう。x座標とy座標の組み合わせがまとまって、座標という意味のある実体となっています。
2の実例は先ほどのお金がいい例です。異なる通貨間での比較を、1つのクラスにまとめることに成功しています。
ほかにもあるかもしれませんが、私にはちょっと思いつきません。何かありましたらコメントで教えてください。

まとめ

より良いオブジェクト指向を目指しましょう(`・ω・´)

money.rb
# 最終的なコード
class Money
  include Comparable
  attr_reader :amount, :currency
  EXCHANGE_RATES = {'USD' => {'USD' => 1, 'JPY' => 103}, 'JPY' => {'USD' => 0.0097, 'JPY' => 1}}

  def initialize(amount, currency)
    @amount, @currency = amount, currency
  end

  def <=>(other_money)
    return nil unless other_money.is_a?(Money)
    if self.currency == other_money.currency
      self.amount <=> other_money.amount
    else
      self.amount <=> other_money.exchange_to(@currency).amount
    end
  end

  def exchange_to(currency)
    rate = EXCHANGE_RATES[@currency][currency].to_f
    Money.new(@amount * rate, currency)
  end
end

参考資料

http://www.sitepoint.com/value-objects-explained-with-ruby/
http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

27
24
1

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
27
24