LoginSignup
10
8

More than 5 years have passed since last update.

どんなメソッドにもコールバックをつけたい (Ruby on Rails)

Last updated at Posted at 2017-12-21

TL;DR

after_save などRails(ActiveRecord)のコールバックメソッドと同じようなテイストでメソッド呼び出しに対してコールバックを設定できるようにする方法について。

ケース

NazrinというCloudsearchクライアントgemがあります。

説明は作者の方の記事に譲ります。
Amazon CloudSearch用Gem、Nazrinをつくった

このgemを使うと、モデルのインスタンスに add_to_index (Cloudsearchにレコードを登録する)などのメソッドが生えます。
これらのメソッドが実行されたあとに追加で処理を行いたいなぁという用事があり、 after_save コールバックと同じノリで after_add_to_index コールバックなど作れないかなぁと思いたったのです。

まぁ、他にもやりようはいくらでもあるのですがせっかくなのでCallbackクラスなどを活用して実現してみようと。

実装

目標

このようなクラスがあります。

class SampleKlass
    def foo
     puts "foo"
    end
end

このクラスの foo メソッドに対して after_foo コールバックをセットすることを目標としましょう。

コールバッククラス

これは本題から少しそれるのですが、コールバック処理が複雑になる場合はコールバック処理をコールバッククラスに分離するとよいそうです。もっと知りたい方は、こちらをどうぞ。四年前の記事ですが、とても参考になります。

てめえらのRailsはオブジェクト指向じゃねえ!まずはCallbackクラス、Validatorクラスを活用しろ!

コールバッククラスには、セットしたいコールバックの種類(今回の場合は after_foo )と同名でメソッドを定義しておきます。

class SampleCallbackKlass
  def after_foo(record)
    puts "after_foo"
  end
end

フックを起こす

foo メソッドは現状実行されてもその実行が感知されることはありません。コールバックを起こすには、 foo が実行されたよ!という通知(フック)を飛ばして上げる必要があります。通知をあげるにはこのようにします。

run_callbacks(:hoge) do
  hoge
end

run_callbacks メソッドの引数にフックの名前、ブロック引数に実行したい処理を渡します。メソッド名とフック名は同一でなくてもかまいません。ここで注意すべきは after_hogehoge はフック名としての hoge ということです。

今回は、 foo メソッドを持つどんなクラスにも対応可能にするため、includeすると foo メソッドがフックを飛ばすようになるモジュールを作ります。

module SampleHooks
  extend ActiveSupport::Concern

  included do
    method = :foo

    orig = "#{method}_without_hook".to_sym

    alias_method orig, method

    define_method(method) do |*args, &block|
      run_callbacks(method) do
        send(orig, *args, &block)
      end
    end
  end
end

アラウンドエイリアスを使い、 foo メソッドがフックを飛ばすようにしました。

コールバックをセットする

では、実際にフックを受けてコールバックが実行されるようにしましょう。
以下の二行をSampleKlassの定義に追加します。

define_callbacks :foo, scope: %i[kind name]
set_callback :foo, :after, SampleCallbackKlass.new

一行目の define_callbacks はクラスに :after_foo コールバックをサポートさせるおまじないで、二行目の set_callback が具体的にそのコールバックはどんな処理なのか割り当てるおまじないです。
scope: %i[kind name] を省略すると、コールバックの名前が before とか after になってしまい何に対するコールバックなのかわかりづらくなります。なので、つけたほうがよいでしょう。

完成

require 'active_model'
require 'active_support'

module SampleHooks
  extend ActiveSupport::Concern

  included do
    method = :foo

    orig = "#{method}_without_hook".to_sym

    alias_method orig, method

    define_method(method) do |*args, &block|
      run_callbacks(method) do
        send(orig, *args, &block)
      end
    end
  end
end

class SampleCallbackKlass
  def after_foo(record)
    puts "after_foo"
  end
end

class SampleKlass
  def foo
    puts "foo"
  end

  extend ActiveModel::Callbacks

   # fooメソッドの定義後じゃないとダメ
  include SampleHooks

  define_callbacks :foo, scope: %i[kind name]
  set_callback :foo, :after, SampleCallbackKlass.new
end

s = SampleKlass.new
s.foo

以上のファイルを実行すると、

foo
after_foo

と表示されるはずです!
foo のあとに after_foo がちゃんと実行されました。

10
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8