问题描述:

I'm writing a library which wraps methods with spies (basically transparently allows you to add callbacks to methods). I currently have it working fine for dealing with bound methods

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)

So far I've noticed a couple things:

FakeClass.methods.include? :age

# => false

FakeClass.instance_method :age

# => #<UnboundMethod FakeClass#age>

Which leads me to my question:

Since instance_method returns an UnboundMethod, how can I define a replacement method to override that (e.g. I'd like a define_instance_method)

Also how can I wrap the UnboundMethod so that it is bound to the instance when the instance is created?

Can I do any of this without messing with the initialize method?

EDIT:

With Ajedi32's advice I was able to change my method to something like this (the API is slightly more complex in my library, but you'll get the gist):

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

EDIT:

Link to my gem in the wild. This is the main entry point (API) which interacts with Core and then instantiates a new Instance https://github.com/jbodah/spy_rb/blob/a40ed2a67b088bfb7d40d12c5b6ffc882a5097c8/lib/spy/api.rb

网友答案:

define_method actually does define an instance method by default. The only reason it's defining a class method instead in your case is because you're calling it inside of instance_eval. (For details on the exact behavior of instance_eval, you may want to read this article). A simpler way to do what you want in that first case is to define a singleton method on the receiver object using 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 []

As for your on_any_instance method, you can use the regular define_method method for that. You will have to call it using send though, since define_method is private. And since the original method you're trying to call is an UnboundMethod, you'll have to bind it to the instance you want to call it on before calling it:

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 []
网友答案:

So it turns out I can do this:

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

But I need to see how I can wrap the method now

相关阅读:
Top