フック

実行するプログラムコードに対して割込み、処理を捕捉することを"フック"と言うが、Rubyでは、いわゆるフックを実装するのも非常に簡単だ。

例えば、任意のクラスのメソッドにフックして、前処理、後処理を挿入してみよう。

  • hook1.rb
class String 
  alias_method :old_split, :split
  def split(arg)
    puts "before split: #{self.inspect}"
    result = old_split(arg)
    puts "after  split: #{arg.inspect} -> result #{result.inspect}"
    result
  end
end

p "foobar".split(//)
  • 実行結果
>ruby hook1.rb
before split: "foobar"
after  split: // -> result ["f", "o", "o", "b", "a", "r"]
["f", "o", "o", "b", "a", "r"]

非常に簡単だ。この例では'alias_method'メソッドにより、クラスのメソッドの別名を定義して、元々のメソッドが呼ばれる前と後にそれぞれ処理を挿入することを可能にしている。(メソッド名が文字列ではなくシンボルなのは、Ruby内部でメソッド名はシンボルとして管理されているからだ)

これをもう少し実用的にしてみよう。今度は任意のクラスの任意のメソッドに対して、これまた任意の前処理(before)と後処理(after)を挿入するメソッドを用意してみよう。これも短いコードで書くことができる。

  • hook2.rb
def addHook(clazz, method, before, after)
  clazz.instance_eval do
    original = instance_method(method)
    define_method(method) do |*args|
      before.call(self, args)
      result = original.bind(self).call(*args)
      after.call(args, result)
      return result
    end
  end
end

addHook String, :split, lambda{|s, *| puts "before split: #{s.inspect}"}, lambda{|s, r| puts "after  split: #{s.inspect} -> result #{r.inspect}"}

p 'foobar'.split(//)
  • 実行結果
>ruby hook2.rb
before split: "foobar"
after  split: [//] -> result ["f", "o", "o", "b", "a", "r"]
["f", "o", "o", "b", "a", "r"]

この例では、メソッドの別名を定義するのではなく、パラメタで与えられたクラスのインスタンスメソッドを'define_method'メソッドで再定義し、その中で元のメソッドの実行前後にラムダで与えられた前処理と後処理を実行している。※

ここで既に分かると思うが、addHookメソッドはAOPにおけるメソッド(メッセージ)インターセプタとして使えるのだ。Rubyはそもそも組込みの機能でAOPでやりたいことの殆どが実現できてしまう。これを見てしまうとJavaC#で実現しているAOPが、どうしても大袈裟に見えてしまう。

どんな言語でもそうだが、言語自体もソフトウェアである以上、元々組込まれていない、想定されていない機能を追加するとゴテゴテして元々の簡素さを失ったり、複雑さが増えたり、大袈裟になってしまいがちだ。これは仕方が無いが、後発の言語ほど開発者に好まれる理由がここにある。
Rubyは今となってはそれほど新しい言語という訳ではないが、それでも他の言語が失ったシンプルさを未だ保っている。がしかし、Javaの登場から現在までの経緯を見ていると、(オープンソースで有り続けている所からして、Javaとは違う向きだが)Rubyがエンタープライズ用途で使われるようになった時にどうなっているかが心配である。

※例ではlambdaを使用しているが、同様のことがprocでもできる。両者は何が違うんだろう。