My use case was to provide the same functionality on instance and class level upon include. By default
include only adds instance methods and
extend only adds class methods. Of course you could do both but that just doesn’t look DRY…
… right? It is especially ugly to give the instructions to the users of your module to do both.
A common idiom
Looking for the solution I stumbled upon
self.included mentioned as “A common idiom” for this case. As per ruby-doc it is a “Callback invoked whenever the receiver is included in another module or class”.
Messing around with it you can do the following:
Which will create
class_method in the class where it’s included. Or to fit my use case and add all methods as class methods:
Which will do an extend as well upon include.
There’s also a similar callback for extend by the by way:
How to make it better
So we have a solution but is it the best one? The first time it took me a while to figure out how it works. Callbacks of this kind are bad in the sense that they describe when they will be executed and NOT what they will do. I wish we could rename the callback to
CreateClassMethodsUponInclude but unfortunately that’s not possible.
So let’s do almost that! We know that it has a fixed name and must be in a module. The module of course can have a custom name and can later be included wherever we want to have this logic. That’s great! We can have something like this:
Well that’s pretty descriptive. OK. But what should happen when we include
CreateClassMethodsUponInclude? It should create the
self.included that does what we want. We can achieve that using some more meta-magic.
Go ahead and copy it into your console. While it works you may think that it’s uglier than what we started with. So..
What did we gain?
The actual implementation may be ugly but no one has to look at it again because we wrapped it into a module that is meaningful and reusable. Using the
CreateClassMethodsUponInclude module no-one will have any excessive thinking to do while reading or writing this piece of logic.
This module is part of the free and public reflection-utils gem. While it’s only one actual use case I think this design pattern is worth following for similar problems.