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_hoge
の hoge
はフック名としての 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
がちゃんと実行されました。