It happened again. While reading through ActiveModel I realized that I had skipped over something cool in ActiveSupport, so I’m going to circle back and cover it.
It’s about the journey and not the destination anyway, right? And so gracious reader, we return to…
Ye Olden Days
Back in the days before we were all travelling West with Rails 4, in fact, way before them, you would find many ways discussed as best practices to extend the functionality of a Class. There is a great summary of approaches on Yehuda’s blog which I’ll just point at and leave their review as an exercise for the reader.
Clearly, with this much confusion, having a blessed way to do this would make
everyone’s life a wee bit better, and so Rails extended Ruby once again with
Here is the before picture (from Rails’ docs):
And what the above looks like with
In addition to a clean interface that standardized the approach, it also enabled automatic dependency resolution which we will see as we…
Read the Source!
Now that you now what features this delivers, let us take a gander at how it’s implemented.
Rather than trim out boring bits, I’ve included the whole source of the module. Everything to safely add both Class and Instance methods (and all relevant dependencies) are in the above. Some review…
Callback invoked whenever the receiver is included in another module or class. This should be used in preference to Module.append_features if your code wants to perform some action when a module is included in another.
Looking then at the code above, we see that there’s an exception thrown if it encounters something like:
A condition it easily detects via an instance variable it sets on the first one.
Called when this module extends another. In the case of
Concern it just uses this as an initialization hook for the creation of the
Ok, on to the real logic!
When this module is included in another, Ruby calls append_features in this module, passing it the receiving module in mod. Ruby’s default implementation is to add the constants, methods, and module variables of this module to mod if this module has not already been added to mod or one of its ancestors. See also Module#include.
So let’s say we had a scenario where we had some top level class Zoo, which includes module Foo, which folds in Bar, and where Bar secretly depends on Baz. Like so:
If we inspect this hypothetical tree, we’ll see that
@_dependencies points to the next level of the tree:
And that every method was similarly added to
So the big question is, how does this code work? Let’s take my example above and hack in some debugging statements:
Running this, we see the following:
Which makes it even more obvious than the code (which wasn’t very obvious if
I’m being quite honest). The bookkeeping on dependent modules is only
triggered when we decorate the eventually inclusive class (
Since it is the destination and doesn’t have a
@_dependencies set, it
finally goes down the other branch to trigger a bunch of
Easy to see in retrospect, but I needed to “lab” this one.
Have fun reading the source!