はじめに
調べ物をしていたら、複数カラムをバリューオブジェクトに構成することで仮想的に一つのカラムとして扱う、composed_of
なるものを見つけたので備忘録します。
住所-よくあるパターン
class User
attr_accessor :city_address, :town_address, :building_address
def address
city_address + town_address + building_address
end
end
attr_accessor
の部分は実際にはカラムになってると思ってください。
上のパターンにはいくつかの問題があります。
-
address
のような整形用メソッドがモデルを汚してしまう - 本来意味を持つのは
address
なのに、メソッドで返ってくるのは単なる文字列 - 一旦
address
を取得しても、そこからcity_addressを取り出すのは容易ではない
住所-改善されたパターン
根本的な問題は、address
は本当はAddress
クラスのオブジェクトであるべきなのにDBの都合でそうなっていない、という点にあります。そこで、address
メソッドでAddress
クラスのオブジェクトを返すような実装が思い浮かびます。
class User
attr_accessor :city_address, :town_address, :building_address
def address
Address.new(city_address, town_address, building_address)
end
end
class Address
attr_reader :city, :town, :building
def initialize(city, town, building)
@city, @town, @building = city, town, building
end
end
ここで、Address
クラスが単なるRubyオブジェクト(PORO, Plain Old Ruby Object)であることが重要です。これで、address
自体が意味を持ちますし、city
の取り出しもできます。
住所-composed_ofを使うパターン
address
メソッドはいいのですが、どうにも単純なメソッドです。これはもしかして、 Rails Wayで楽ができるのではないでしょうか。
class User
attr_accessor :city_address, :town_address, :building_address
composed_of :address, mapping: [%w(city_address city), %w(town_address town), %w(building_address building)]
end
class Address
attr_reader :city, :town, :building
def initialize(city, town, building)
@city, @town, @building = city, town, building
end
end
composed_of
を使うことで、よりシンプルに書けるようになりました。
mapping
オプションには、配列の配列を指定します。内側の配列は2要素で、まずカラム名を、次にバリューオブジェクトの属性名を指定します。これにより、user.city_address
がuser.address.city
にマップされるようになります。
まとめ
composed_of
を使ってみたわけですが、実は重要なのはcomposed_of
ではなく、バリューオブジェクトの方なのかな、と思ったりします。要は、アプリケーション内でそれ自体の意味があるものをそれ自体のクラスにくくりだすということです。
これは最近思っていることですが、「アプリケーション内でPOROが多く使われているほど、そのアプリケーションは見通しが良い」ということは考えられないでしょうか。この点についてはもっと熟達した方々の意見を待ちたいと思います。
【追記:参考資料】
http://api.rubyonrails.org/classes/ActiveRecord/Aggregations/ClassMethods.html
http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/