Help us understand the problem. What is going on with this article?

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

repro
世界59か国6,500以上の導入実績を持つCE(カスタマーエンゲージメント)プラットフォーム「Repro(リプロ)」を提供
https://repro.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away