< Prev^ UpNext >

A Better Spy

Our first spike works fine, and actually would be good enough to start with, but I don't like it, on a couple fronts.

First, we have no good way to respect one of our notes: 'Make sure the name(s) of the new methods don't collide with existing methods.' There are enough hooks in Ruby that we could scan existing method names and pick a unique one, but that wouldn't protect us from that name being taken by someone else later.

Second, we're tied to Kernel. This isn't an immediate problem, because we're only wrapping Kernel::require, but our approach doesn't scale well. If we ever want to wrap a method in some other module or class, we'll need to open up that module or method as well. Our current approach gives us no good way to do that on the fly, either; we'd have to modify the wrapping code.

So let's see if we can do better. It turns out that Ruby provides enough hooks that we can.

def wrap(klass, method_id)
  original_method= klass.method(method_id)
  klass.define_method(method_id) do |*args|
    puts "doing #{method_id}(#{args.join(', ')})"
    return_value= original_method.call(*args)
    puts "orignal #{method_id} returned #{return_value}"
    return_value
  end
end

wrap(Kernel, :require)

We define a new method, wrap, which takes a class and a method id (a keyword). (It's a Ruby convention to use the name klass when defining a variable which you'd really like to call class.) The first thing it does is call method on the passed-in class to grab a copy of the method we're going to wrap. Then it calls define_method to create our wrapped version. The block passed in to define-method becomes the code for our wrapper.

Finally, we use our new method to wrap Kernel#require.

Using the same target app as our first spike, when we run the new version of our wrapping code, we get the same result:

jmax@deepthought $ ruby -r './wrap_require.rb' target_app.rb
doing require(socket)
doing require(socket.so)
orignal require returned true
doing require(io/wait)
orignal require returned true
orignal require returned true
In the subject app
jmax@deepthought $

This nicely clears up both issues; by using method and define_method, instead of alias_method, we aren't adding any new method definitions to the target class, and we also can pass any class we like in to wrap.

The code for this spike is in the git repo, under spikes/dynamic_wrap_require.

< Prev^ UpNext >