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
.