2008-05-04
■[ruby]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"
- 25 http://reader.livedoor.com/reader/
- 20 http://www.rubyist.net/~kazu/samidare/
- 7 http://fastladder.com/reader/
- 6 http://www.google.co.jp/ig?hl=ja
- 5 http://blog.livedoor.jp/dankogai/archives/51040709.html
- 5 http://d.hatena.ne.jp/piyo2-moko/20080503
- 5 http://r.hatena.ne.jp/katsu3/Ruby/
- 4 http://a.hatena.ne.jp/smeghead/mobile
- 4 http://blog.livedoor.jp/dankogai/
- 4 http://hanataba-present.seesaa.net/