Ruby方法拦截

时间:2010-09-23 14:30:34

标签: ruby introspection

我想拦截ruby-class上的方法调用,并且能够在实际执行方法之前和之后执行某些操作。我尝试了以下代码,但得到错误:

  

MethodInterception.rb:16:in before_filter': (eval):2:in alias_method':undefined method   say_hello' for class功课”   (NameError)           来自(eval):2:在`before_filter'

有人可以帮我做对吗?

class MethodInterception

  def self.before_filter(method)
    puts "before filter called"
    method = method.to_s
    eval_string = "
      alias_method :old_#{method}, :#{method}

      def #{method}(*args)
        puts 'going to call former method'
        old_#{method}(*args)
        puts 'former method called'
      end
    "
    puts "going to call #{eval_string}"
    eval(eval_string)
    puts "return"
  end
end

class HomeWork < MethodInterception
  before_filter(:say_hello)

  def say_hello
    puts "say hello"
  end

end

3 个答案:

答案 0 :(得分:14)

我想出了这个:

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,从而导致了尝试重命名不存在的方法。

解决方案很简单:不要那样做!

好吧,好吧,也许不那么简单。您可能只是强迫您的客户在定义了他们的方法之后始终致电before_filter 。但是,这是糟糕的API设计。

因此,您必须以某种方式安排您的代码推迟方法的包装,直到它实际存在。这就是我所做的:不是在before_filter方法中重新定义方法,而是仅记录以后要重新定义的事实。然后,我在method_added挂钩中重新定义实际

这里有一个小问题,因为如果你在method_added内添加一个方法,那么当然会立即再次调用它并再次添加方法,这将导致它再次被调用,并且等等。所以,我需要防止递归。

请注意,此解决方案实际上在客户端上执行排序:如果您在before_filter ,那么OP的版本有效em>定义方法,只有在之前调用时,我的版本才有效。但是,它很容易扩展,因此它不会遇到这个问题。

另请注意,我做了一些与问题无关的其他更改,但我认为更多的是Rubyish:

  • 使用mixin而不是类:继承是Ruby中非常有价值的资源,因为您只能从一个类继承。然而,Mixins很便宜:你可以根据需要混合使用。另外:你真的可以说Homework IS-A MethodInterception?
  • 使用Module#define_method代替evaleval是邪恶的。 '努夫说。 (绝对没有理由在OP的代码中首先使用eval。)
  • 使用方法换行技术而不是alias_methodalias_method链技术使用无用的old_fooold_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

答案 1 :(得分:2)

较少的代码从原来改变了。我只修改了2行。

class MethodInterception

  def self.before_filter(method)
    puts "before filter called"
    method = method.to_s
    eval_string = "
      alias_method :old_#{method}, :#{method}

      def #{method}(*args)
        puts 'going to call former method'
        old_#{method}(*args)
        puts 'former method called'
      end
    "
    puts "going to call #{eval_string}"
    class_eval(eval_string) # <= modified
    puts "return"
  end
end

class HomeWork < MethodInterception

  def say_hello
    puts "say hello"
  end

  before_filter(:say_hello) # <= change the called order
end

这很有效。

HomeWork.new.say_hello
#=> going to call former method
#=> say hello
#=> former method called

答案 2 :(得分:0)

JörgWMittag的解决方案非常好。如果你想要更强大的东西(经过充分测试),那么最好的资源就是rails回调模块。