2010-11-18 69 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!