在Ruby中覆盖方法调用?

时间:2010-02-03 16:26:26

标签: ruby metaprogramming

我正在尝试在调用特定类的任何方法时获得回调。 覆盖“发送”不起作用。似乎发送不会在正常的Ruby方法调用中调用。以下面的例子为例。

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

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

我正在尝试做什么?

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

10 个答案:

答案 0 :(得分:12)

使用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 << self语法重写了您的代码以打开元类,因为我不确定如何在类方法上使用alias_method

答案 1 :(得分:4)

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

(还有rcapture http://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

答案 2 :(得分:2)

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

答案 3 :(得分:1)

你可以做这样的事情,你甚至可以在被调用的方法上设置条件(我不认为这是有用的,但你仍然只是为了以防万一)。

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

希望这有效。

答案 4 :(得分:1)

你试图挂钩一个类的实例方法吗?然后下面的代码片段可能会有所帮助。它使用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!

答案 5 :(得分:0)

答案 6 :(得分:0)

我没有完整的答案,但我认为method_added可能对此有所帮助。

答案 7 :(得分:0)

我使用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

答案 8 :(得分:0)

我的方法是将我正在尝试使用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

答案 9 :(得分:0)

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

希望这会有所帮助。