概要
最近読んだRuby: Module.#prepend でメソッド呼び出しをフックという記事の手法をベースにメソッドをフックしてログを仕込んだりする手法を考えてみます。
この記事の手法を利用すると、任意のクラスの任意のメソッドをフックして、前後に処理を挿入することができます。
フックは好きな順番、好きな個数を設定できます。
人によっては、これをAOP(アスペクト指向プログラミング)のような何かだと考えるかもしれません。
実装手順
- Objectを拡張して、フックを簡単に追加できるようにする。
- フックする処理を記述するモジュール(以降、フックモジュールと呼ぶ)を定義する。
- フックモジュールをObjectに登録し、任意のクラスにフック処理を追加できるようにする。
- フックしたい対象のクラスに、フックしたいメソッドを指定した記述を追加する。
1. Objectにフック追加メソッドを定義
- Objectクラスにフックモジュールを登録する特異メソッド
register_hook
を定義する。-
regist
とか書くと原理主義者の邪教徒狩りに合うのでregister
と書きます。
-
-
register_hook
は「フックモジュールをinclude
し、引数で渡されたメソッドをフックモジュールに登録する」モジュールを生成するメソッドを定義する(・・・ややこしい)。
class << Object
def register_hook(hook_module)
define_method('hook_' + hook_module.name) do |methods|
Module.new do |m|
include hook_module
hook_module.register methods
end
end
end
end
2. フックモジュールを定義
フックモジュールは以下の要件を満たす。
-
register
という特異メソッドを持つ。 -
register
はシンボルの配列を受け取り、シンボルで与えられる名前のメソッドを定義する。 -
register
で定義されたメソッドは、シンボルで与えられる名前の元のメソッドを呼び出す。 - その他、細かいことは以下のサンプルを参照。
# メソッドの呼び出し前後にログを仕込むフックモジュール
module StartEndLogger
def self.register(methods)
methods.each do |method|
define_method(method) do |*args|
begin
log method, 'start >>> '
super(*args) # フックしたメソッドの呼び出し
ensure
log method, 'end <<< '
puts
end
end
end
end
def log(method, mark)
msg = mark
msg << self.to_s
msg << '.' << method.to_s
puts msg
end
end
3. フックモジュールをObjectに登録
Objectクラスでregister_hook
を呼び出し、フックモジュールを登録する。
これで、任意のクラスでフック処理を追加できるようになる。
class << Object; register_hook(StartEndLogger) end
4. 対象クラスにフック処理を追加
-
regisiter_hook
によって生成されたメソッド(ここではhook_StartEndLogger
)を呼び出し、生成されたモジュールをprepend
する。 -
hook_StartEndLogger
にはフック対象のメソッド名をシンボルとして渡す。
class Widget
prepend hook_StartEndLogger %i(introduce_me rename sum)
def initialize(name); @name = name end
def introduce_me; puts "I'm a #{@name}" end
def rename(name); @name = name end
def sum(*args); args.reduce(:+) end
end
使用例
実際に実行してみます。
w = Widget.new 'Freezer'
w.introduce_me
w.sum 1, 2, 3
w.rename 'Cooler'
w.introduce_me
# => start >>> #<Widget:0x1940ac0>.introduce_me
# => I'm a Freezer
# => end <<< #<Widget:0x1940ac0>.introduce_me
# =>
# => start >>> #<Widget:0x1940ac0>.sum
# => end <<< #<Widget:0x1940ac0>.sum
# =>
# => start >>> #<Widget:0x1940ac0>.rename
# => end <<< #<Widget:0x1940ac0>.rename
# =>
# => start >>> #<Widget:0x1940ac0>.introduce_me
# => I'm a Cooler
# => end <<< #<Widget:0x1940ac0>.introduce_me
おまけ:複数のフックを追加
フック処理は複数追加できます。
ログを勝手に取るだけではシツレイなので、アイサツを追加します。
アイサツは大事。イイネ?
# メソッド呼び出し前に挨拶
module Greeter
def self.register(methods)
methods.each do |method|
define_method(method) do |*args|
puts "Domo #{method}=san, Hook-module desu."
super(*args)
end
end
end
end
# Greeterを全クラスで登録可能にする
class << Object; register_hook(Greeter) end
# フック対象
class Widget
prepend hook_StartEndLogger %i(introduce_me rename sum)
prepend hook_Greeter %i(introduce_me sum)
def initialize(name); @name = name end
def introduce_me; puts "I'm a #{@name}" end
def rename(name); @name = name end
def sum(*args); args.reduce(:+) end
end
事項結果がこちら。
w = Widget.new 'Freezer'
w.introduce_me
w.sum 1, 2, 3
w.rename 'Cooler'
w.introduce_me
# => ドーモ introduce_me=サン, Greeterデス。
# => start >>> #<Widget:0x19906e0>.introduce_me
# => I'm a Freezer
# => end <<< #<Widget:0x19906e0>.introduce_me
# =>
# => ドーモ sum=サン, Greeterデス。
# => start >>> #<Widget:0x19906e0>.sum
# => end <<< #<Widget:0x19906e0>.sum
# =>
# => 6
# => start >>> #<Widget:0x19906e0>.rename
# => end <<< #<Widget:0x19906e0>.rename
# =>
# => ドーモ introduce_me=サン, Greeterデス。
# => start >>> #<Widget:0x19906e0>.introduce_me
# => I'm a Cooler
# => end <<< #<Widget:0x19906e0>.introduce_me