Cracking the Box Open with Module Factories
About the lesser-known Ruby Module Factories
Metaprogramming is one of Ruby’s most powerful, intriguing and hard-to-grasp features. Ruby has a deep kind of truth that I could not yet find in any language, similar to the chicken & egg dilemma that we find in real life: an object is generated from a class, but a class is an object itself. Once you understand the truth behind this statement, everything makes perfect sense: from the beautiful object model to the expressive lines of code we can achieve with the language.
This post discusses a very useful feature that only a few Ruby programmers know: the module factory. Before getting to that, we will dig into the object model to acknowledge how modules work under the hood, then we will walk through some possible use cases of modules and how these compare to module factories.
An example to start off
Suppose we have a Ruby on Rails application with a polymorphic Image
model as a single source of truth for all images.
Initially, requirements demand some of our models to be linked to one main_image
each. To solve this problem, we can define an Imageable
module that knows how to extract the right record from the images
table and deliver an Image
instance.
A good use case for modules is to share simple glue functionality among similar objects, especially when it feels like the functionality belongs to the object itself.
Setting aside has_one
relationships for a reason, this is the simplest code that solves our problem:
And using the module is very straightforward:
Now Project
is set to have_many
images, and we can quickly obtain the main_image
of any instance as if it was a normal attribute. Don’t worry about how records arrive in the database, assume that only a getter does the work we need just fine.
When the truth is unveiled
But guess what? It turns out that module Imageable
is nothing but pure syntactic sugar provided by Ruby, and what the language is doing under the hood is creating an instance of the Module
class. Let’s visualize the same code with another pair of eyes:
You can think of modules as objects with a particular kind of power: they allow you to write a collection of methods that can be shared among classes and other modules. The way to share this collection is via the include
statement:
"extend"
does the same job as “include,” but it acts in a different context. We will skip “extend” in this post.
When you “define” a method in a module, what you are really doing is writing it to a special kind of “template,” a collection called “instance methods.” Think of it as a super power conceded by the language, something that only module instances can do.
By including the module in a class, you are just “mixing in” its instance methods into the class’ own instance methods collection:
And what about classes? A class
is just a specialization of a module
with factory powers; hence it’s able to do something the latter can’t do: create objects provided with these instance methods.
A module is just a stripped-down class, but it possesses the most basic functionality any class needs: the ability to hold instance methods.
But wait, isn’t a module an object? Let’s get back to the deeper truth:
Getting deeper with Module Factories
Since a module is an object, it must have been generated from a class. And what class is it? Module
. Remember the Module.new
above? Classical object oriented programming, huh? The definition of a Module
exists somewhere under the covers, and it might as well look like this:
Ruby defines “Module” automatically for us, right when our program boots up.
However, this is not just an object model fairy tale: Actually, it’s sort of ordinary at a language level. The Module
class is a factory, right? Because it’s a class, we can subclass it to create our own factories:
And we can instantiate a custom module just like any other object:
In this example, the initializer of the Module
class got overwritten with a new one that defines instance methods on-the-fly in every created module! Otherwise, it would have been set in stone forever:
Or, more traditionally:
With the dynamic version, we can also create a module that responds to four
, five
and six
on-the-fly, see?
The conventional initialize
method is a key factor to building great module factories. Without it, our factory would have looked kinda strange:
The ModuleGenerator
example isn’t particularly useful, so let’s revisit the Imageable
concern.
Module Factories for the greater good
Our requirements for Imageable
have changed, and now some of our models need other kinds of one to one
image fields. A single main_image
field won’t do anymore.
To solve this problem, we can "convert” Imageable
from a module instance to a module factory:
Now we can have as many image fields as we want in our model:
But we can do even better! Since we are using a class to create a module, why not use private methods to organize our logic?
It’s great that we are now able to name each chunk of code, thanks to our good ’n’ old class organization tools. Now it’s easier to grow our logic without turning it into a mess.
Of course, non-dynamic methods can use module_eval
instead of define_method
for better readability:
The dirty block approach
I’ve seen a few projects solving the same problem roughly like this:
The “Module” class’ constructor provided by Ruby can evaluate a block with the same effect as “module_eval”.
As you can see, a “factory” singleton method is being defined within a module, and it returns another module with dynamically generated instance methods.
The problem with this code is that it tends to grow disorderly and out of bounds. Imagine two, three, four, ten additional dynamic method definitions within the same block of code. Would you be able to tell exactly what they are, or even what they do? Probably so, but it would take more time to figure it out. Comments would likely help, but they wouldn’t exempt things from being ugly.
With module factories, we can use private methods to aid in organization, whereas with the dirty block approach we simply can’t. And this is a problem.
The macro approach
The “macro” approach is a familiar style used throughout Rails and other projects: the idea is to provide the target class with singleton methods that are responsible for defining other methods within the class’ instance collection. Let’s change Imageable
to use this approach:
This code is not bad per se, but there’s still a potential problem with it: suppose one of the macros needs to define more than one instance method. In that case, we would have gotten stuck with chunks of complex code we wouldn’t be able to name, and it would be harder to read it overall.
We could split the logic to other macros, but that would pollute the target class’ interface with methods it wouldn’t ever need to use! Moreover, we would still be polluting the class with singleton methods anyway (has_one_image
), so it’s not an approach I would call “tidy”.
The upside of macros is that they are readable, but if you’ve seen Rails models in real life you’ve probably noticed they tend to be full of these nifty calls that read like DSLs, and it may get to a point where you don’t know which macro belongs to which module anymore.
The advantages of Module factories
Module factories are relatively unusual in the Ruby world, but I’d wish they weren’t because they solve some problems in a very elegant fashion:
- They allow to organize on-the-fly instance methods definitions.
- They are tidy and don’t pollute the target class with singleton methods.
- They provide encapsulation for the factory logic.
- They are self-contained: all instance methods can be written directly in the module.
- They can be as readable as macros.
- You can pass options to a plain old constructor of a plain old class. Now you know to which module a feature belongs, instead of trying to guess what’s the origin of a macro you find in a class body, amidst a mess of other macros.
Wrap up
Metaprogramming is cool, but it should be used with care. It’s a very powerful feature appropriate to aid in framework code, even though there are nice use cases regarding application logic. I even tried to illustrate a good example with Imageable
.
And you should also think twice before using modules as “concerns” (as known in the Rails community). There are usually better ways to solve the same problem, like object composition, for example. Nevertheless, it’s a no-brainer for simple glue code and convenient attribute emulation.
That said, if all you have is a hammer, everything looks like a nail.
I hope you have enjoyed this post. If you have any questions, just hit me up in the comments!