我刚刚想到了这个:
module MethodInterception
def method_added(meth)
return unless (@intercepted_methods ||= []).include?(meth) && !@recursing
@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
您的代码中的基本问题是您在 before_filter 方法中重命名了该方法,但是在您的客户端代码中,您在实际定义该方法之前调用了 before_filter,从而导致尝试重命名一个方法不存在的。
解决方案很简单:Don't Do That™!
嗯,好吧,也许没那么简单。你可以简单地强制你的客户总是在他们定义了他们的方法之后调用before_filter。但是,这是糟糕的 API 设计。
因此,您必须以某种方式安排您的代码,以延迟方法的包装,直到它实际存在。这就是我所做的:我没有在before_filter 方法中重新定义方法,而是只记录了稍后要重新定义的事实。然后,我在method_added 钩子中进行实际重新定义。
这里有个小问题,因为如果你在method_added里面添加一个方法,那么当然会马上再次被调用,再添加一个方法,导致它被再次调用,以此类推.所以,我需要提防递归。
请注意,此解决方案实际上也在客户端上强制执行排序:而 OP 的版本仅在您调用 before_filter 后有效定义方法,我的版本只有在你调用它之前才有效。但是,它很容易扩展,因此不会遇到这个问题。
还请注意,我做了一些与问题无关的额外更改,但我认为这些更改更加 Rubyish:
- 使用 mixin 而不是类:继承在 Ruby 中是非常宝贵的资源,因为您只能从一个类继承。然而,mixins 很便宜:你可以随意混合。另外:你真的可以说 Homework IS-A MethodInterception 吗?
- 使用
Module#define_method 而不是eval:eval 是邪恶的。 '纳夫说。 (首先,在 OP 的代码中,绝对没有任何理由使用 eval。)
- 使用方法包装技术代替
alias_method:alias_method 链技术用无用的old_foo 和old_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