给定一个类和一个UnboundMethod定义一个实例方法

时间:2014-11-26 18:41:00

标签: ruby

我正在编写一个用spies包装方法的库(基本上透明地允许你向方法添加回调)。我目前在处理绑定方法时工作正常

class FakeClass
  def self.hello_world
    'hello world'
  end

  def age
    25
  end
end

module Spy
  def self.on(receiver, msg)
    original = receiver.method(msg)
    wrapped = Proc.new {|*args| original.call(*args)}
    original.owner.instance_eval do
      define_method msg, wrapped
    end
  end
end

# works
Spy.on(FakeClass, :hello_world)

# also works
Spy.on(FakeClass.new, :age)

# what I want to do
Spy.on_any_instance(FakeClass, :age)

到目前为止,我注意到了一些事情:

FakeClass.methods.include? :age
# => false
FakeClass.instance_method :age
# => #<UnboundMethod FakeClass#age>

这引出了我的问题:

由于instance_method会返回UnboundMethod,如何定义替换方法以覆盖它(例如,我想要define_instance_method

另外,我如何包装UnboundMethod,以便在创建实例时将其绑定到实例?

我可以在不弄乱initialize方法的情况下执行此操作吗?

编辑:

根据Ajedi32的建议,我能够将我的方法改为这样的(我的库中的API稍微复杂一点,但你会得到要点):

def wrap_original
  context = self
  @original.owner.instance_eval do
    define_method context.msg do |*args|
      if context.original.respond_to? :bind
        result = context.original.bind(self).call(*args)
      else
        result = context.original.call(*args)
      end
      context.after_call(result, *args)
      result
    end
  end
end

def unwrap_original
  context = self
  @original.owner.instance_eval do
    define_method context.msg, context.original
  end
end

编辑:

在野外链接到我的宝石。这是与Core交互然后实例化新实例的主要入口点(API)https://github.com/jbodah/spy_rb/blob/a40ed2a67b088bfb7d40d12c5b6ffc882a5097c8/lib/spy/api.rb

2 个答案:

答案 0 :(得分:1)

define_method实际上确实默认定义了一个实例方法。在你的情况下,它定义一个类方法的唯一原因是因为你在instance_eval内调用它。 (有关instance_eval的确切行为的详细信息,您可能需要阅读this article)。在第一种情况下执行所需操作的更简单方法是使用define_singleton_method在接收器对象上定义单例方法:

module Spy
  def self.on(receiver, msg)
    original = receiver.method(msg)

    # Use `define_singleton_method` to define a method directly on the receiver
    receiver.define_singleton_method(msg) do |*args, &block|
      puts "Calling #{msg} with args #{args}" # To verify it's working
      original.call(*args, &block) # Call with &block to preserve block args
    end
  end
end

Spy.on(FakeClass, :hello_world)
FakeClass.hello_world # Prints: Calling hello_world with args []

f = FakeClass.new
Spy.on(f, :age)
f.age # Prints: Calling age with args []

对于您的on_any_instance方法,您可以使用常规define_method方法。您必须使用send来调用它,因为define_method是私有的。由于您尝试调用的原始方法是UnboundMethod,因此在调用它之前,您必须bind将其调用到要调用它的实例:

module Spy
  def self.on_any_instance(receiver, msg)
    original = receiver.instance_method(msg) # Get `UnboundMethod`

    # We must call `define_method` with `send`, since it's private
    receiver.send(:define_method, msg) do |*args, &block|
      puts "Calling #{msg} with args #{args}"

      # Method gets bound to `self`, the current instance
      original.bind(self).call(*args, &block)
    end
  end
end

Spy.on_any_instance(FakeClass, :age)
FakeClass.new.age # Prints: Calling age with args []

答案 1 :(得分:0)

事实证明我可以做到这一点:

original = FakeClass.instance_method(:age)
FakeClass.instance_eval {:define_method, :age, original}

但我需要看看我现在如何包装方法

相关问题