【问题标题】:Overriding method calls in Ruby?覆盖Ruby中的方法调用?
【发布时间】:2010-02-03 16:26:25
【问题描述】:

我试图在调用特定类的任何方法时获得回调。 覆盖“发送”不起作用。似乎 send 在正常的 Ruby 方法调用中没有被调用。举个例子。

class Test
  def self.items
   @items ||= []
  end
end

如果我们在 Test 上覆盖 send,然后调用 Test.items,则不会调用 send。

我正在尝试做的事情可能吗?

我宁愿不使用 set_trace_func,因为它可能会大大减慢速度。

【问题讨论】:

  • 可以通过将类别名为我自己的类,并将方法调用委托给原始类来做到这一点。
  • Protip:所有告诉您如何覆盖单个方法的答案也告诉您如何对类实现的所有方法执行此操作。你只需要遍历它们。这是你真正的问题吗?

标签: ruby metaprogramming


【解决方案1】:

使用aliasalias_method

# the current implementation of Test, defined by someone else
# and for that reason we might not be able to change it directly
class Test
  def self.items
    @items ||= []
  end
end

# we open the class again, probably in a completely different
# file from the definition above
class Test
  # open up the metaclass, methods defined within this block become
  # class methods, just as if we had defined them with "def self.my_method"
  class << self
    # alias the old method as "old_items"
    alias_method :old_items, :items
    # redeclare the method -- this replaces the old items method,
    # but that's ok since it is still available under it's alias "old_items"
    def items
      # do whatever you want
      puts "items was called!"
      # then call the old implementation (make sure to call it last if you rely
      # on its return value)
      old_items
    end
  end
end

我使用class &lt;&lt; self 语法重写了您的代码以打开元类,因为我不确定如何在类方法上使用alias_method

【讨论】:

  • 不,我想要 any 方法调用的回调。项目只是一个例子。
  • 用你所有的方法去做吧。
  • 结合:method_added 应该可以满足您的需求。
【解决方案2】:

类似这样:与实例方法和类方法一起使用,它不仅会拦截类中定义的当前方法,还会拦截以后通过重新打开类等添加的任何方法。

(还有rcapturehttp://code.google.com/p/rcapture/):

module Interceptor
  def intercept_callback(&block)
    @callback = block
    @old_methods = {}
  end
  def method_added(my_method)
    redefine self, self, my_method, instance_method(my_method)
  end
  def singleton_method_added(my_method)
    meta = class << self; self; end
    redefine self, meta, my_method, method(my_method)
  end
  def redefine(klass, me, method_name, my_method)
    return unless @old_methods and not @old_methods.include? method_name
    @old_methods[method_name] = my_method
    me.send :define_method, method_name do |*args|
      callback = klass.instance_variable_get :@callback
      orig_method = klass.instance_variable_get(:@old_methods)[method_name]
      callback.call *args if callback
      orig_method = orig_method.bind self if orig_method.is_a? UnboundMethod
      orig_method.call *args
    end
  end
end

class Test
  extend Interceptor
  intercept_callback do |*args|
    puts 'was called'
  end
  def self.items
    puts "items"
  end
  def apple
    puts "apples"
  end
end

class Test
  def rock
    puts "rock"
  end
end

Test.items
Test.new.apple
Test.new.rock

【讨论】:

  • 如果您将method_namecaller*args 之前或代替*args 之前传递给回调,然后说intercept_callback do |method_name, by_caller| ; puts "#{method_name} called by #{by_caller.first}" ; end,则更有用
【解决方案3】:

您可以看到这是如何通过 ExtLib 挂钩功能完成的。 ExtLib::Hook 基本上允许您在方法完成之前或之后调用任意回调。请参阅此处 GitHub 上的代码了解其完成方式(它会覆盖 :method_added 以在将方法添加到类时自动重写方法)。

【讨论】:

    【解决方案4】:

    你可以做这样的事情,你甚至可以对被调用的方法设置条件(我认为这没什么用,但你还是有它以防万一)。

    module MethodInterceptor
    
      def self.included(base)
        base.extend(ClassMethods)
        base.send(:include, InstanceMethods)
        base.class_eval do 
          # we declare the method_list on the class env
          @_instance_method_list = base.instance_methods.inject(Hash.new) do |methods, method_name|
            # we undef all methods
            if !%w(__send__ __id__ method_missing class).include?(method_name)
              methods[method_name.to_sym] = base.instance_method(method_name)
              base.send(:undef_method, method_name)
            end
            methods
          end
        end
      end
    
      module ClassMethods
    
        def _instance_method_list
          @_instance_method_list
        end
    
        def method_added(name)
          return if [:before_method, :method_missing].include?(name)
          _instance_method_list[name] = self.instance_method(name)
          self.send(:undef_method,  name)
          nil
        end
    
      end
    
      module InstanceMethods
    
        def before_method(method_name, *args)
          # by defaults it always will be called
          true
        end
    
        def method_missing(name, *args)
          if self.class._instance_method_list.key?(name)
            if before_method(name, *args) 
              self.class._instance_method_list[name].bind(self).call(*args)
            else
              super
            end
          else
            super
          end
        end
      end
    
    end
    
    class Say
      include MethodInterceptor
    
      def before_method(method_name, *args)
        # you cannot say hello world!
        return !(method_name == :say && args[0] == 'hello world')
      end
    
      def say(msg)
        puts msg
      end
    
    end
    

    希望这行得通。

    【讨论】:

      【解决方案5】:

      你想钩住一个类的实例方法吗?那么下面的 sn-p 可能会有所帮助。它使用可以通过安装的 RCapture

      gem install rcapture
      

      介绍文章可以在here找到

      require 'rcapture'
      
      class Test 
        include RCapture::Interceptable
      end
      
      Test.capture_post :class_methods => :items do
        puts "items!"
      end
      
      Test.items 
      #=> items!
      

      【讨论】:

      • 来自著名的 RCapture 开发者本人!
      【解决方案6】:

      【讨论】:

      • 不幸的是,这只适用于您指定的特定方法。它只需覆盖它们即可工作。
      【解决方案7】:

      我没有完整的答案,但我想method_added 在这里可能会有所帮助。

      【讨论】:

        【解决方案8】:

        我已经使用 Proxy 类让它工作 - 然后使用真实类的名称设置一个常量。我不确定如何让它与实例一起使用。有没有办法改变哪些对象变量也指向?

        基本上,我想这样做:

        t = Test.new
        Persist.new(t)
        
        t.foo # invokes callback
        

        这是我用来让它与类一起工作的代码:

        class Persist
          class Proxy
            instance_methods.each { |m| 
              undef_method m unless m =~ /(^__|^send$|^object_id$)/ 
            }
        
            def initialize(object)
              @_persist = object
            end
        
            protected
              def method_missing(sym, *args)
                puts "Called #{sym}"
                @_persist.send(sym, *args)
              end
          end
        
        
          attr_reader :object, :proxy
        
          def initialize(object)
            @object = object
            @proxy  = Proxy.new(@object)
            if object.respond_to?(:name)
              silence_warnings do
                Object.const_set(@object.name, @proxy)
              end
            end
          end
        end
        

        【讨论】:

        【解决方案9】:

        我的方法是用一个简单地回调原始对象的 Logger shell 对象来包装我试图记录的对象。下面的代码通过将要记录的对象包装在一个类中,该类只调用底层对象上所需的任何方法,但提供了一种捕获这些调用并记录(或其他)每个访问事件的方法..

        class Test
          def self.items
            puts "  Class Items run"
            "Return"
          end
        
          def item
            puts "  Instance item run"
            return 47, 11
          end
        end
        
        class GenericLogger
          @@klass = Object # put the class you want to log into @@klass in a sub-class
          def initialize(*args)
            @instance = @@klass.new(*args)
          end
          def self.method_missing(meth, *args, &block)
            retval = handle_missing(@@klass, meth, *args, &block)
            if !retval[0]
              super
            end
            retval[1]
          end
        
          def method_missing(meth, *args, &block)
            retval = self.class.handle_missing(@instance, meth, *args, &block)
            if !retval[0]
              super
            end
            retval[1]
          end
        
          def self.handle_missing(obj, meth, *args, &block)
            retval = nil
            if obj.respond_to?(meth.to_s)
              # PUT YOUR LOGGING CODE HERE
              if obj.class.name == "Class"
                puts "Logger code run for #{obj.name}.#{meth.to_s}"
              else
                puts "Logger code run for instance of #{obj.class.name}.#{meth.to_s}"
              end
              retval = obj.send(meth, *args)
              return true, retval
            else
              return false, retval
            end
          end
        end
        
        # When you want to log a class, create one of these sub-classes 
        # and place the correct class you are logging in @@klass
        class TestLogger < GenericLogger
          @@klass = Test
        end
        
        retval = TestLogger.items
        puts "Correctly handles return values: #{retval}"
        tl = TestLogger.new
        retval = tl.item
        puts "Correctly handles return values: #{retval}"
        
        begin
          tl.itemfoo
        rescue NoMethodError => e
          puts "Correctly fails with unknown methods for instance of Test:"
          puts e.message
        end
        
        begin
          TestLogger.itemsfoo
        rescue NoMethodError => e
          puts "Correctly fails with unknown methods for class Test"
          puts e.message
        end
        

        该代码示例的输出是:

        Logger code run for Test.items
          Class Items run
        Correctly handles return values: Return
        Logger code run for instance of Test.item
          Instance item run
        Correctly handles return values: [47, 11]
        Correctly fails with unknown methods for instance of Test:
        undefined method `itemfoo' for #<TestLogger:0x2962038 @instance=#<Test:0x2962008>>
        Correctly fails with unknown methods for class Test
        undefined method `itemsfoo' for TestLogger:Class
        

        【讨论】:

          【解决方案10】:

          singleton_method_added可以给你一个简单的解决方案:

          class Item
            @added_methods = []
            class << self
              def singleton_method_added name
                if name != :singleton_method_added && !@added_methods.include?(name)
                  @added_methods << name
                  pMethod = self.singleton_method name
                  self.singleton_class.send :define_method, name do |*args, &blk|
                  puts "Callback functions calling..."
                  pMethod.call(*args, &blk)
                end
              end
            end
          
            def speak
              puts "This is #{self}"
            end
          end
          

          希望这会有所帮助。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2012-12-14
            • 1970-01-01
            • 1970-01-01
            • 2011-12-13
            • 1970-01-01
            • 1970-01-01
            • 2022-01-23
            • 1970-01-01
            相关资源
            最近更新 更多