我想在某些课程中发生某些事情时收到通知。我想以这样的方式设置它,使得我在这些类中的方法的实现不会改变。
我以为我会有以下模块:
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
方法。)
我注意到我可能想要使用“模块+超级技巧”。我不确定这是什么 - 也许有人可以启发我?
答案 0 :(得分:15)
已经有一段时间了,因为这里的问题已经激活,但还有另一种方法可以通过包含(或扩展)模块来包装方法。
从2.0开始,你可以prepend一个模块,有效地使它成为前置类的代理。
在下面的示例中,调用扩展模块模块的方法,传递要包装的方法的名称。对于每个方法名称,都会创建并预先添加一个新模块。这是为了简化代码。您还可以将多个方法附加到单个代理。
使用alias_method
和instance_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!"
第一种方法需要修改方法(你说你不想要的东西),第二种方法需要使用代理,也许是你不想要的东西。没有简单的解决方案,我很抱歉。