2010-11-18 177 views
3

我想在某些課程發生某些事情時收到通知。我想以這樣的方式設置它,以便在這些類中實現我的方法不會改變。如何通過包含模塊來包裝調用Ruby方法?

我想我有類似如下模塊:

module Notifications 
    extend ActiveSupport::Concern 

    module ClassMethods 
    def notify_when(method) 
     puts "the #{method} method was called!" 
     # additional suitable notification code 
     # now, run the method indicated by the `method` argument 
    end 
    end 
end 

然後我就可以像這樣把它混成我的課:

class Foo 
    include Notifications 

    # notify that we're running :bar, then run bar 
    notify_when :bar 

    def bar(...) # bar may have any arbitrary signature 
    # ... 
    end 
end 

我的鑰匙願望是,我不您不得不修改:bar以使通知正常運行。這可以做到嗎?如果是這樣,我將如何編寫notify_when實現?

此外,我使用Rails 3,所以如果有ActiveSupport或其他技術我可以使用,請隨時分享。 (我看着ActiveSupport::Notifications,但這需要我來修改bar方法。)


它已經到了我的注意,我可能要使用「模塊+超級帽子戲法」。我不確定這是什麼 - 也許有人可以啓發我?

回答

7

我想你可以使用別名方法鏈。

事情是這樣的:

def notify_when(method) 
    alias_method "#{method}_without_notification", method 
    define_method method do |*args| 
    puts "#{method} called" 
    send "#{method}_without_notification", args 
    end 
end 

您不必自己修改方法,使用這種方法。

+0

如果:方法接受一個塊? – 2010-11-18 20:47:02

+0

您不必使用'define_method',您可以通過'eval'來定義方法。例如。 ''eval「def#{method}(* args,&block)...' – 2010-11-18 20:49:40

+0

這要求'notify_when'出現在方法定義之後,所以它不會在你的代碼示例中工作,並且它修改了'bar '方法,但是除了使用代理對象外,你還可以修改(裝飾)你的方法。 – horseyguy 2010-11-18 21:05:39

3

我能想到的兩種方法:

(1)裝點Foo方法包括通知。

(2)使用攔截方法調用Foo並通知您,當它們發生

第一個解決方案是由的Jakub採取的辦法,雖然alias_method解決方案是不是實現這一目標的最佳途徑的代理對象,用這個來代替:

def notify_when(meth) 
    orig_meth = instance_method(meth) 
    define_method(meth) do |*args, &block| 
    puts "#{meth} called" 
    orig_meth.bind(self).call *args, &block 
    end 
end 

第二種方法則需要您結合使用method_missing與代理:

class Interceptor 
    def initialize(target) 
    @target = target 
    end 

    def method_missing(name, *args, &block) 
    if @target.respond_to?(name) 
     puts "about to run #{name}" 
     @target.send(name, *args, &block) 
    else 
     super 
    end 
    end 
end 

class Hello; def hello; puts "hello!"; end; end 

i = Interceptor.new(Hello.new) 
i.hello #=> "about to run hello" 
     #=> "hello!" 

第一種方法需要修改方法(你說你不想要的東西),第二種方法需要使用代理,也許你不想要的東西。我很抱歉沒有簡單的解決方案。

+0

請問你可以添加一個解釋,爲什麼重新綁定原始方法比使用alias_method更好?這個差別對我來說並不明顯,謝謝 – kostja 2014-06-24 19:43:26

+0

Nevermind,我找到了[這個很好的解釋](http://stackoverflow.com/questions/4470108)。 – kostja 2014-06-24 19:52:22

11

這個問題已經有一段時間了,因爲這裏的問題一直很活躍,但還有另一種方法可以通過包含(或擴展)的模塊來包裝方法。

從2.0開始,你可以prepend一個模塊,有效地使它成爲預先分類的代理。

在下面的示例中,調用擴展模塊模塊的方法,傳遞要包裝的方法的名稱。對於每個方法名稱,都會創建並預先創建一個新模塊。這是爲了簡化代碼。您還可以將多個方法附加到單個代理。

與使用alias_methodinstance_method(稍後綁定在self上)解決方案的一個重要區別是您可以在定義方法本身之前定義要包裝的方法。

module Prepender 

    def wrap_me(*method_names) 
    method_names.each do |m| 
     proxy = Module.new do 
     define_method(m) do |*args| 
      puts "the method '#{m}' is about to be called" 
      super *args 
     end 
     end 
     self.prepend proxy 
    end 
    end 
end 

用途:

class Dogbert 
    extend Prepender 

    wrap_me :bark, :deny 

    def bark 
    puts 'Bah!' 
    end 

    def deny 
    puts 'You have no proof!' 
    end 
end 

Dogbert.new.deny 

# => the method 'deny' is about to be called 
# => You have no proof!