Hooking Instance Methods in Ruby
Everyone and their dog is familiar with ActiveRecord-style callbacks, you know, the kind where you specify you want a particular method or proc to be run before or after a given event on your model. It helps you enforce the principles of code ownership while making it trivial to do the hardwiring, ensuring that code owned by the model is also managed by the model.
I love this kind of programming and recently found that I needed some similar functionality in a particular class, one that wasn’t tied to Active_[Insert your railtie here]. My case was different in that I knew that _any class inheriting from a particular base, which we’ll call
HookBase , needed a hardwired hook for every method defined, functionality that needed to run for virtually every instance method call. The following example illustrates my need:
So, operating from the idea that every instance method extending classes implement should have default wrap-around behavior, I got to work. First off, you need to know that ruby has built-in lifecycle hooks on your classes, objects, and modules. Things like
method_added help you hook in to your code to ensure that the appropriate things are happening on your classes, objects, and modules. So in my case, I needed to know when a method was added to
HookBase (or one of its children) so that I could appropriately tap into that code.
method_added is where the meat of the solution lies. When a method is added, ruby fires the
method_added call on the object (if any exists), passing it the name of the new method. Keep in mind that this happens after the method has already been created, which is crucial to this solution. We’ll next create a new name from the old name, prepended with some identifier (in this case we chose “hide_”).
We’ll need to check the
private_instance_methods array for already defined method names to ensure we’re not duplicating our effort (or clobbering someone elses), as well as checking our own array constant for methods we don’t want to hook. Remember that
method_added will be called on every method that is found for HookBase as well as children. I found that there were HookBase methods I had implemented that were supporting this behavior and didn’t need to be hardwired, so I added this to my list of methods to ignore.
If we’ve made it this far, go ahead and alias the old method to the new one, then privatize the new one. Now we can safely redefine the old method without destroying the code it contained. We also now know that no one (except self) can invoke the private method directly, they’ll have to implicitly go through the HookBase first.
Redefining the old method is as simple as using
define_method and calling our
hardwired_hook method within, passing our
new_method (which is privatized), and the old method (for convenience), and any associated arguments and blocks.
The final implementation looks something like this:
The great thing about this approach is you may not even care about hardwiring anything, but just want to provide hooking functionality. If that’s the case, simply define a class method in HookBase to register a hook (such as
after ), optionally accepting an
:except list of methods. Internally store the blocks passed and invoke them in the
hardwired_hook method either before or after the method call.
Let me know if you have any comments or different approaches. Happy hacking!
UPDATE: Forgot that
method_added needs to be defined in
class << self to work properly. Also updated to use the
Array#& intersection method I described in Intersecting arrays in ruby instead of using