Ruby Method Auditing Using Module#prepend
Yesterday I was interviewing for a Ruby developer position at a company here in Denver when my interviewer posed a good metaprogramming challenge: he asked me to write an automated method auditor.
He described a class method that would take the name of an instance method and generate an automatic, logged paper trail any time that target method is called. Something like this:
That would produce output that looked like this:
I came up with a solution that worked but involved some potentially dangerous hacks to Object#send
. Before presenting my solution, I said “this is probably a bad idea, but…” and I was right. Good ideas rarely start out that way.
The solution that he proposed involved using the often-overlooked Module#prepend
and I thought it was cool enough to warrant a write-up.
Module Magic
The strategy was to use the behavior of Module#prepend
to create a dynamically-generated wrapper method for Messenger#share
.
In that wrapper, we’ll have one “Performing” message fire off before the body of the method is executed, the method execution itself, and then a final “Exiting” message once the method’s execution has completed successfully.
But to understand how to use Module#prepend
to do this, we first need a good understanding of how method inheritance works in Ruby.
Ancestor Chains
Each Ruby object sits at the end of what’s called an Ancestor Chain. It’s a list of the ancestors objects that the object inherits from. Ruby uses this Ancestor Chain to determine which version of a method (if any at all) is executed when the object receives a message.
You can actually view the ancestor tree for any object by calling Module#ancestors
. For example, here’s the ancestor chain for our Messenger
class:
When we #include
or #prepend
a module into a class, Ruby makes changes to that classes’ ancestor chain, tweaking which methods are found and in what order. The big difference between #include
and #prepend
is where that change is made. Let’s create a module called Auditable
which will (eventually) hold the code that does our auditing magic:
If we use Module#include
to import the (currently nonexistent) methods from Auditable
, Ruby will squeeze that module in as an ancestor of our Messenger
class. Here, see for yourself:
When we call a method on Messenger
, Ruby will look at what’s defined in the Messenger
class and — if it can’t find a method that matches the call — climb up the ancestor chain to Auditable
to look again. If it doesn’t find what it’s looking for on Auditable
it moves on to Object
and so forth.
That’s #include
. If we instead use Module#prepend
to import the contents of the module, we get a totally different effect:
#prepend
makes Messenger
an ancestor of Auditable
. Wait. What?
This might look like Messenger
is now a superclass of Auditable
, but that’s not exactly what’s happening here. Instances of class Messenger
are still instances of class Messenger
but Ruby will now look for methods to use for Messenger
in the Auditable
module before looking for them on the Messenger
class itself.
And that, friends, is what we’ll be taking advantage of to build this auditor: if we create a method called Auditable#share
, Ruby will find that before it finds Messenger#share
. We can then use a super
call in Auditable#share
to access (and execute!) the original method defined on Messenger
.
The Module Itself
We’re not actually going to create a method called Auditable#share
. Why not? Because we want this to be a flexible utility. If we hard-code the method Auditable#share
, we’ll be able to use it only on methods that implement the #share
method. Or worse, we’d have to re-implement this same auditor pattern for every method we ever want to audit. No thanks.
So instead, we’re going to define our method dynamically and the audit_method
class method to fire it off:
When audit_method
is called in an implementing class, a method is created in the Auditable
module with the same name as the method-to-audit. In our case, it will create a method called Auditable#share
. As mentioned, Ruby will find this method before it finds the original method on Messenger
because we’re prepending the Auditable
module in the implementing class.
That means that we can use a super call to reach up the ancestor chain and execute Messenger#send
. When we do so, we pass the arguments we’ve collected (*arguments
) up the chain too.
Once we’ve called the original method, we print our exit message and call it a day. Good work, gang!
Bringing it all Together
Now it’s just a matter of prepend
ing this module to our Messenger
class and we should be good to go:
And good golly it works:
The implications here are huge for auditing, but there’s more to this trick. You can used prepending to change the behavior of objects without changing the objects themselves. This is an adaptable and clear way to create higher-order components in Ruby. Performance testing, error handling. You can do a lot here.
Conclusion
Ruby modules are more complicated than most people think. It’s not as simple as “dumping” code from one place to another, and understanding those differences unlocks some really neat tools for your utility belt.
You may have noticed that I only talked about Module#include
and Module#prepend
today, and that I didn’t touch on Module#extend
. That’s because it works very differently than its cousins. I’ll write up an in-depth explanation of Module#extend
soon to complete the set.
For now, if you want to learn more I’d recommend reading Ruby modules: Include vs Prepend vs Extend by Léonard Hetsch. It was really helpful in putting all of this together.