LoginSignup
25
20

More than 5 years have passed since last update.

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

Posted at

はじめに

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
25
20
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
25
20