aliasによるメソッドの再定義は危険なのでUnboundMethodかextendを使おう
Jay Fields' Thoughts: Alternatives for redefining methods
メソッドの再定義の技法はいろいろあるが、どれも欠点があるというお話。状況に応じて使い分けるべき。
aliasで再定義
メソッドを再定義するときにこんな感じでaliasで元のメソッドをコピーするのは常套手段だ。
class Gateway def process(document) p "gateway processed document: #{document}" end end class Gateway alias old_process process def process(document) p "do something else" old_process(document) end end Gateway.new.process("hello world") # >> "do something else" # >> "gateway processed document: hello world"
しかし、これは危険だ。なぜなら、そのスクリプトを再度読み込むと、無限ループに陥ってしまうからだ!だいたい、old_*などというメソッドを作成してるあたりがカッコ悪い。
以下のスクリプトを実行してみるとよくわかる。無限ループになってしまうため、「do something else」が延々と表示されてしまう。
class Gateway def process(document) p "gateway processed document: #{document}" end end class Gateway alias old_process process def process(document) p "do something else" old_process(document) end end class Gateway alias old_process process def process(document) p "do something else" old_process(document) end end Gateway.new.process("hello world") # >> "do something else" # >> "do something else" # >> "do something else" # >> "do something else" # 略
UnboundMethodを使う
UnboundMethodとdefine_methodのコンボも結構有名なテクニック。
Module#instance_methodでUnboundMethodが返る。ぶっちゃけレシーバがないメソッド。それを変数に代入しておく。古い定義のUnboundMethod版だ。そして、Module#define_methodでメソッドを再定義する。define_methodはクロージャーなので外側の変数にもアクセスできる。だから古い定義を参照できる。UnboundMethodを呼ぶにはUnboundMethod#bindでレシーバを設定してMethod#callで呼ぶ。
UnboundMethodはアンパンマンの顔の部分で、UnboundMethod#bindはアンパンマンに新しい顔をつけてもらうようなイメージだと思う?
class Gateway def process(document) p "gateway processed document: #{document}" end end class Gateway process_method = instance_method(:process) undef :process # warningがうざいので黙らせる define_method :process do |document| p "do something else" process_method.bind(self).call(document) end end Gateway.new.process("hello world") # >> "do something else" # >> "gateway processed document: hello world"
これもスクリプトの再ロードで問題がおきるけど、無限ループよりはましで、「do something else」がだぶる。
class Gateway def process(document) p "gateway processed document: #{document}" end end class Gateway process_method = instance_method(:process) undef :process # warningがうざいので黙らせる define_method :process do |document| p "do something else" process_method.bind(self).call(document) end end class Gateway process_method = instance_method(:process) undef :process # warningがうざいので黙らせる define_method :process do |document| p "do something else" process_method.bind(self).call(document) end end Gateway.new.process("hello world") # >> "do something else" # >> "do something else" # >> "gateway processed document: hello world"
UnboundMethod(再ロード対応版)
onceメソッドを定義して、ブロック内のコードを一度しか実行させないようにしてみれば再ロード問題が解決する。
class Gateway def process(document) p "gateway processed document: #{document}" end end def once unless instance_variable_defined? :@__once_executed__ yield @__once_executed__ = true end end class Gateway once do process_method = instance_method(:process) undef :process # warningがうざいので黙らせる define_method :process do |document| p "do something else" process_method.bind(self).call(document) end end end class Gateway once do process_method = instance_method(:process) undef :process # warningがうざいので黙らせる define_method :process do |document| p "do something else" process_method.bind(self).call(document) end end end Gateway.new.process("hello world") # >> "do something else" # >> "gateway processed document: hello world"
しかし、それでも完璧な解決方法ではなく、クロージャーなのでdefによるメソッド定義よりも多少遅くなるという問題がある。メモリリークの可能性もでてくる。
extendとsuper
再定義といったら、やっぱりextendとsuperを使うのが一番rubyらしくてカッコイイと思う。
しかも再ロードしても問題ない。
唯一の欠点といえば、extendしなければならないということなのだが、大したことはないんじゃないかな。
class Gateway def process(document) p "gateway processed document: #{document}" end end module ProcessLogging def process(document) p "do something else" super end end Gateway.new.extend(ProcessLogging).process("hello world") # >> "do something else" # >> "gateway processed document: hello world"