【问题标题】:Ruby method interceptionRuby 方法拦截
【发布时间】:2011-04-16 07:40:33
【问题描述】:

我想拦截对 ruby​​ 类的方法调用,并能够在方法实际执行之前和之后做一些事情。我尝试了以下代码,但得到了错误:

MethodInterception.rb:16:in before_filter': (eval):2:inalias_method': 未定义的方法 say_hello' for classHomeWork' (名称错误) 来自 (eval):2:in `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

【问题讨论】:

    标签: ruby introspection


    【解决方案1】:

    我刚刚想到了这个:

    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
    

    【讨论】:

    • 注意:alias_method 会污染命名空间,但使用 alias_method + send 会比获取方法的引用更快(在我的测试中大约快 50%)。
    【解决方案2】:

    Jörg W Mittag 的解决方案非常好。如果您想要更健壮的东西(经过良好测试),最好的资源是 rails 回调模块。

    【讨论】:

    • 他说他在用 Rails 吗??!
    • 在 Jörg 的示例中(包括家庭作业类),我数了不到 50 行代码。当然,我们可以想出一个策略来测试它,直到我们认为它健壮且经过良好测试。
    • @banister:不知道 Swanand 的疯狂想法从何而来。只有 98% 的 ruby​​ 人使用 Rails。
    • 回调模块驻留在 Rails 的哪个部分?主动支持?
    • @banister:他从来没有!我将他引向 Rails,因为它具有类似的功能,他可以从中学习/复制。 @Andrew::-),也是。
    【解决方案3】:

    从原始代码中更改了更少的代码。我只修改了 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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-05-23
      • 1970-01-01
      • 2014-10-11
      • 1970-01-01
      • 2012-04-01
      • 2011-01-04
      • 2015-04-29
      相关资源
      最近更新 更多