はじめに
調べ物をしていたら、複数カラムをバリューオブジェクトに構成することで仮想的に一つのカラムとして扱う、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/