我只是想出了這一點:
module MethodInterception
def method_added(meth)
return unless (@intercepted_methods ||= []).include?(meth) && [email protected]
@recursing = true # protect against infinite recursion
old_meth = instance_method(meth)
define_method(meth) do |*args, &block|
puts 'before'
old_meth.bind(self).call(*args, &block)
puts 'after'
end
@recursing = nil
end
def before_filter(meth)
(@intercepted_methods ||= []) << meth
end
end
使用它,像這樣:
class HomeWork
extend MethodInterception
before_filter(:say_hello)
def say_hello
puts "say hello"
end
end
作品:
HomeWork.new.say_hello
# before
# say hello
# after
在你的代碼的基本問題是,你改名爲中的方法10方法,但在客戶端代碼中,在方法實際定義之前調用before_filter
,從而導致嘗試重命名不存在的方法。
解決方案很簡單:不要那樣做™!
好吧,好吧,也許不是那麼簡單。你可能只是迫使你的客戶經常before_filter
後他們已經定義了他們的方法。但是,這是糟糕的API設計。
所以,你必須以某種方式安排你的代碼推遲方法的包裝,直到它真正存在。這就是我所做的:不是重新定義方法中的方法,而是隻記錄稍後重新定義的事實。然後,我做實際重新定義在method_added
掛鉤。
這裏有一個小問題,因爲如果你在method_added
裏面添加一個方法,那麼它當然會立即被調用並再次添加該方法,這將導致它被再次調用,依此類推。所以,我需要防範遞歸。
注意,這種解決方案實際上也強制客戶端上的排序:減少運算的版本只有作品如果你打電話before_filter
後定義方法,如果你之前調用它我的版本纔有效。但是,擴展起來非常容易,所以它不會遇到這個問題。
還請注意,我做了一些不相關的問題的其他變化,但我認爲更Rubyish:
- 用而不是類一個mixin:繼承是非常寶貴的資源在Ruby中,因爲你只能從一個類繼承。然而,Mixins很便宜:您可以隨意混合。此外:你真的可以說家庭作業IS-A MethodInterception?
- 使用
Module#define_method
而不是eval
:eval
是邪惡的。 「Nuff說。 (在OP的代碼中,絕對沒有任何理由首先使用eval
。)
- 使用方法包裝技術而不是
alias_method
:alias_method
鏈技術污染了無用的old_foo
和old_bar
方法的名稱空間。我喜歡我的命名空間。
我只是修正了一些我上面提到的限制,並增加了一些功能,但我懶得重寫我的解釋,所以我在這裏重新發布修改後的版本:
module MethodInterception
def before_filter(*meths)
return @wrap_next_method = true if meths.empty?
meths.delete_if {|meth| wrap(meth) if method_defined?(meth) }
@intercepted_methods += meths
end
private
def wrap(meth)
old_meth = instance_method(meth)
define_method(meth) do |*args, &block|
puts 'before'
old_meth.bind(self).(*args, &block)
puts 'after'
end
end
def method_added(meth)
return super unless @intercepted_methods.include?(meth) || @wrap_next_method
return super if @recursing == meth
@recursing = meth # protect against infinite recursion
wrap(meth)
@recursing = nil
@wrap_next_method = false
super
end
def self.extended(klass)
klass.instance_variable_set(:@intercepted_methods, [])
klass.instance_variable_set(:@recursing, false)
klass.instance_variable_set(:@wrap_next_method, false)
end
end
class HomeWork
extend MethodInterception
def say_hello
puts 'say hello'
end
before_filter(:say_hello, :say_goodbye)
def say_goodbye
puts 'say goodbye'
end
before_filter
def say_ahh
puts 'ahh'
end
end
(h = HomeWork.new).say_hello
h.say_goodbye
h.say_ahh
這很簡單而漂亮。 – Swanand 2010-09-23 15:42:45
一個說明:alias_method污染名稱空間,但使用alias_method + send會比獲取對該方法的引用更快(在我的測試中快大約50%)。 – 2013-03-04 07:57:33