Posted at
RubyDay 25

Real World Refinements

More than 3 years have passed since last update.

Ruby AdventCalendarの最終日です。

締切をオーバーしました……。orz

Refinementsの使い道が無いとか、使ったことがないという人が結構居るみたいなので、いくつかのユースケースを紹介しようと思います。


テストコードのDSL

一つはrspec-parameterized

# 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

https://github.com/tomykaira/rspec-parameterized/blob/master/lib/rspec/parameterized/table_syntax.rb

こういう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したり出来るので、各クラス向けの実装を別のファイルに分割することは可能だと思います。