Ruby AdventCalendarの最終日です。
締切をオーバーしました……。orz
Refinementsの使い道が無いとか、使ったことがないという人が結構居るみたいなので、いくつかのユースケースを紹介しようと思います。
テストコードのDSL
# Table Syntax Style (like Groovy spock)
# Need ruby-2.1 or later
describe "plus" do
using RSpec::Parameterized::TableSyntax
where(:a, :b, :answer) do
1 | 2 | 3
5 | 8 | 13
0 | 0 | 0
end
with_them do
it "should do additions" do
expect(a + b).to eq answer
end
end
end
こういうGroovyのspockの様にテストパラメーターを記述するDSLをRefinementsで定義しています。
RSpecのdescribe
は単なるクラス定義なので、using
を使って特定のテストケース上でだけ動くDSLが書けます。
最近は使われなくなったshould
とかも、Refinementsを使えば割と安全に実装できると思いますが、今更感はありますね。
実はk-tsj/power_assertでも、グローバルメソッドキャッシュを破棄するという目的のためにrefinementsを利用しているコードがあるんですが、流石に例として特殊過ぎる気はします。
Concernモジュール
もう一つはConcernモジュールでの利用です。
例えば、ActiveRecordでレコードがcreateされた時に、fluentdにレコードの内容を送りたい場合を考えてみます。
module FluentLoggable
extend ActiveSupport::Concern
module MethodDefinitions
refine User do
def fluentd_tag
"user"
end
def fluentd_payload
as_json.with_indifferent_access.merge(
groups: groups.map { |g| g.fluentd_payload }
)
end
end
refine Group do
def fluentd_tag
"group"
end
def fluentd_payload
name
end
end
end
using MethodDefinitions
included do
after_create :post_to_fluentd
end
private
def post_to_fluentd
Fluent::Logger.post(fluentd_tag, fluentd_payload)
end
end
ActiveSupport.run_load_hooks :FluentLoggable, FluentLoggable
class Class
def lazy_include(hook_name)
klass = self
ActiveSupport.on_load hook_name do |mod|
klass.include mod
end
end
end
class User < ActiveRecord::Base
lazy_include :FluentLoggable
end
こんな感じで書いておくと、モジュールによって定義されるpost_to_fluentd
の中でだけ、refineされたメソッドが有効になります。
この書き方のメリットは、あるオブジェクトをRootとするツリー構造があった時に、各オブジェクトにpayloadの算出方法の責任を任せつつ、それを他の箇所では一切見せずにこの目的のためにしか使えないようにできる点です。
上記の例以外に考えられるケースとしては、JSON APIの返り値として階層上になったJSONを構築したい時等でしょうか。
Railsアプリケーションを書いていてFat Model化した時に困るのは、あるメソッドがどういう用途で使われてるのか良く分からなくなることです。いざ変更した時に想定外の所がぶっ壊れる可能性もあります。
Refinementsで処理内容を内側に閉じ込めておけば、その範囲でしか使われていないことを明確にできます。
ただ、この書き方には一つ大きな問題があります。
それはRailsにおけるautoloadと非常に相性が悪いことです。
lazy_include
というハックを使っているのはそのためです。
Concernモジュールの定義が完了してからincludeしないと、User
を定義している最中にFluentLoggable
が読み込まれてそこで参照されているUser
というクラスに反応して再度autoloadが走ってcircular dependencyが発生し、エラーになります。
そのため、こういった微妙なハックが必要になります。
この書き方が有効なケースはそれ程多くないし、includeの際にハックを噛まさなければならないという問題もありますが、使い道の一つとしては有り得るんじゃないかと思います。
まあ、私自身、試行錯誤した結果なので、本当にこれが良い使い方か、と言われると良く分からないというのが正直な所です。
ちなみに、refine
部分はmodule_eval
したり他のモジュールをincludeしたり出来るので、各クラス向けの実装を別のファイルに分割することは可能だと思います。