LoginSignup
5
8

More than 5 years have passed since last update.

RubyでメソッドをフックしてBefore/Afterにログを付けたりする

Posted at

概要

最近読んだRuby: Module.#prepend でメソッド呼び出しをフックという記事の手法をベースにメソッドをフックしてログを仕込んだりする手法を考えてみます。

この記事の手法を利用すると、任意のクラスの任意のメソッドをフックして、前後に処理を挿入することができます。
フックは好きな順番、好きな個数を設定できます。

人によっては、これをAOP(アスペクト指向プログラミング)のような何かだと考えるかもしれません。

実装手順

  1. Objectを拡張して、フックを簡単に追加できるようにする。
  2. フックする処理を記述するモジュール(以降、フックモジュールと呼ぶ)を定義する。
  3. フックモジュールをObjectに登録し、任意のクラスにフック処理を追加できるようにする。
  4. フックしたい対象のクラスに、フックしたいメソッドを指定した記述を追加する。

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
5
8
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
5
8