はじめに
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