如何通过包含模块来包装Ruby方法的调用?

时间:2010-11-18 20:31:58

标签: ruby methods module

我想在某些课程中发生某些事情时收到通知。我想以这样的方式设置它,使得我在这些类中的方法的实现不会改变。

我以为我会有以下模块:

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方法。)


我注意到我可能想要使用“模块+超级技巧”。我不确定这是什么 - 也许有人可以启发我?

3 个答案:

答案 0 :(得分:15)

已经有一段时间了,因为这里的问题已经激活,但还有另一种方法可以通过包含(或扩展)模块来包装方法。

从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!

答案 1 :(得分:8)

我想你可以使用别名方法链。

这样的事情:

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

您不必使用此方法自行修改方法。

答案 2 :(得分: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!"

第一种方法需要修改方法(你说你不想要的东西),第二种方法需要使用代理,也许是你不想要的东西。没有简单的解决方案,我很抱歉。