2010-09-23 222 views
5

我想攔截ruby-class上的方法調用,並且能夠在方法的實際執行之前和之後執行某些操作。我試了下面的代碼,但得到錯誤:紅寶石方法截取

MethodInterception.rb:16:in before_filter': (eval):2:in alias_method': undefined method say_hello' for class HomeWork' (NameError) from (eval):2:in `before_filter'

有人能幫我做對嗎?

class MethodInterception 

    def self.before_filter(method) 
    puts "before filter called" 
    method = method.to_s 
    eval_string = " 
     alias_method :old_#{method}, :#{method} 

     def #{method}(*args) 
     puts 'going to call former method' 
     old_#{method}(*args) 
     puts 'former method called' 
     end 
    " 
    puts "going to call #{eval_string}" 
    eval(eval_string) 
    puts "return" 
    end 
end 

class HomeWork < MethodInterception 
    before_filter(:say_hello) 

    def say_hello 
    puts "say hello" 
    end 

end 

回答

2

較少的代碼從原來的更改。我只修改了2行。

class MethodInterception 

    def self.before_filter(method) 
    puts "before filter called" 
    method = method.to_s 
    eval_string = " 
     alias_method :old_#{method}, :#{method} 

     def #{method}(*args) 
     puts 'going to call former method' 
     old_#{method}(*args) 
     puts 'former method called' 
     end 
    " 
    puts "going to call #{eval_string}" 
    class_eval(eval_string) # <= modified 
    puts "return" 
    end 
end 

class HomeWork < MethodInterception 

    def say_hello 
    puts "say hello" 
    end 

    before_filter(:say_hello) # <= change the called order 
end 

這很好。

HomeWork.new.say_hello 
#=> going to call former method 
#=> say hello 
#=> former method called 
14

我只是想出了這一點:

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而不是evaleval是邪惡的。 「Nuff說。 (在OP的代碼中,絕對沒有任何理由首先使用eval。)
  • 使用方法包裝技術而不是alias_methodalias_method鏈技術污染了無用的old_fooold_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 
+0

這很簡單而漂亮。 – Swanand 2010-09-23 15:42:45

+0

一個說明:alias_method污染名稱空間,但使用alias_method + send會比獲取對該方法的引用更快(在我的測試中快大約50%)。 – 2013-03-04 07:57:33

0

JörgW Mittag的解決方案相當不錯。如果你想要更健壯的東西(閱讀良好的測試),最好的資源就是rails回調模塊。

+1

他是否說他正在使用rails?! – horseyguy 2010-09-23 18:16:58

+0

在Jörg的例子中,我算不到50行代碼(包括家庭作業類)。當然,我們可以提出一個策略來測試它,直到我們認爲它穩健並經過充分測試。 – 2010-09-23 20:49:18

+0

@banister:不知道Swanand從哪裏得到了這個瘋狂的想法。只有98%的紅寶石人使用Rails。 – 2010-09-24 03:32:10