Posted at

Ruby: Module.#prepend でメソッド呼び出しをフック

More than 3 years have passed since last update.


はじめに

Module.#prepend でメソッド呼び出しをフックしてみました。


試してみる

フック設定はクラス定義に書きたいので、やりやすいように Class クラスに hook メソッドを作成してみました。


hook.rb

module Hook

def hook(&block)
module_eval { prepend Module.new &block }
end
end

class Class
include Hook
end



試してみます。


try1.rb

class C

hook do # hook 設定
def foo(message)
puts "hook: #{message}"
super
end
end

def foo(message)
puts message
end
end

obj = C.new
obj.foo("hello")


実行結果

$ ruby try1.rb 

hook: hello # hook の出力
hello


多段のフックの例。


try2.rb

class C

hook do # 1つ目のhook 設定
def foo(message)
puts "#{before_message}: #{message}"
super
puts "#{after_message}: #{message}"
end
end

hook do # 2つ目のhook 設定
def foo(message)
puts "hook2: #{before_message}: #{message}"
super
puts "hook2: #{after_message}: #{message}"
end
end

def foo(message)
puts message
end

def before_message
"before!"
end

def after_message
"after!"
end
end

c = C.new
c.foo("hello")


$ ruby tx.rb 

hook2: before!: hello # 2つ目のhookの出力
before!: hello # 1つ目のhookの出力
hello
after!: hello # 1つ目のhookの出力
hook2: after!: hello # 2つ目のhookの出力

クラス継承ですが、上の「try2.rb」の場合、以下のようになります。

hook 毎に動的に生成した Module インスタンスを継承リストに追加するので、2つ追加されています。

p C.ancestors   #=> [#<Module:0x007fac4fdb7510>, #<Module:0x007fac4fdb7628>, C, Object, Kernel, BasicObject]


ロギングに応用した例です。


try3.rb

require 'forwardable'

require 'logger'

class C
hook do # ロギング <-- このコメントが必要なところに'不吉な匂い'が... (「おわりに」で述べます)
extend Forwardable
attr_writer :logger
def logger ; @logger ||= Logger.new(nil) ; end
delegate %i(debug info warn error fatal) => :logger

def foo(message)
info { "call 'foo(#{message})'" } # ログ出力
super
end
def bar(message)
info { "call 'bar(#{message})'" } # ログ出力
super
end
end

def foo(message)
puts message
end

def bar(message)
puts message
end
end

c = C.new
c.logger = Logger.new 'try.log'
c.foo("hello")
c.bar("goodbye")


$ ruby try3.rb 

hello
goodbye

ログの内容


try.log

# Logfile created on 2015-01-26 00:39:51 +0900 by logger.rb/44203

I, [2015-01-26T00:50:39.131894 #19689] INFO -- : call 'foo(hello)'
I, [2015-01-26T00:50:39.132019 #19689] INFO -- : call 'bar(goodbye)'


おわりに

横断的関心事はまとめておくのがよいと思いますが、(私が AOP に詳しくないからか)実用的な例はロギングくらいしか思いつきません。

hook メソッドは「クラス定義のスコープに設定を書きたいけど、不必要に識別子(定数名など)は設けたくない」という理由で考えましたが、ロギングのような用途なら具体的名称で設定したほうがいいように思いました。(ancestors 等で名前も明示されるようになりますし)

class C

module Logging # 名前(Logging)を付けて、素直にモジュール定義する
extend Forwardable
attr_writer :logger
def logger ; @logger ||= Logger.new(nil) ; end
delegate %i(debug info warn error fatal) => :logger
:
end
prepend Logging # prepend する
:

p C.ancestors  #=> [C::Logging, C, Object, Kernel, BasicObject]

hookメソッドの必要性は少なそうです。(思いついた時は、いいかなと思ったんですが。。。)


本稿内容の動作確認は以下の環境で行っています。


  • Ruby 2.1.5 p273