I created a Ruby gem recently, called includable-activerecord. It’s pretty small, but I thought I might explain why I created it, and discuss its implementation.
When you use ActiveRecord, you normally include it in your model like this:
class User < ActiveRecord::Base # ... end
User class is inheriting from the
ActiveRecord::Base class. This is class-based inheritance, also called “classical” inheritance. (That’s “classical” as in “class”, not as a synonym for “traditional”.) Class-based inheritance represents an “is-a” relationship. So we’re saying that a user is an ActiveRecord base. Another way to say this is that
User is a subclass of
There are a few problems with this. First, what is a “base”? The name was chosen because it’s a base class. But just like we don’t give factory classes names like
UserFactory (at least not in Ruby), we shouldn’t name base classes
I suppose that we’re trying to say that this is an ActiveRecord model. That sounds fine at first glance — this is our model for users. But what if we also want to say that a user is a person? Ruby doesn’t allow inheriting from multiple classes. Now we have to choose whether to inherit from
Person makes more sense, because it fills the “is-a” rule better. Class inheritance is intended for a hierarchical “is-a” relationship, such as “a user is a person”, or “a circle is a shape”. But since
ActiveRecord::Base is a base class, we have to use it as our base class.
We could work around this problem by subclassing
ActiveRecord::Base and then subclassing
Person. That’s fine if
Person is also a model that we store in the database. But if that’s not the case, then we have a problem.
Ruby provides another way of implementing inheritance — mixins. We often don’t think of this as an inheritance model, but it really is. When we include a module, that module gets added to the class’s ancestor chain. We can mix in as many modules as we want.
Mixins indicate more of an “acts like” relationship than an “is-a” relationship. It’s for shared behavior between classes that don’t have a hierarchical relationship. For example, when we mix in the
Enumerable module, we’re saying that we want our class to act like other classes that include
Enumerable. That sounds more like what we want ActiveRecord to be. We want our user model to behave like other ActiveRecord models, in the way that they can persist to a database.
But ActiveRecord doesn’t support that. Almost all the other Ruby ORMs do; as we’ve shown above, this is for good reasons.
So I decided to see if I could implement the equivalent of the
ActiveRecord::Base class as a module that could be mixed into model classes. I decided to call my mixin module
ActiveRecord::Model, because classes that mix it in will behave as ActiveRecord models.
It turns out that
ActiveRecord::Base is a pretty complex class. It includes and extends a lot of other modules. Luckily, as of ActiveRecord 4.0, that’s all the code it includes.
The module only defines a single class method,
included. This is one of Ruby’s many hook methods. It gets called when the module in question gets included in another module, and receives that other model as its argument. All we need to have this method do is to
include everything that
ActiveRecord::Base includes, and
extend everything that
ActiveRecord::Base extends. Ruby provides a method that’s defined on all classes, called
included_modules, which we can use to get the list of everything that’s included in
ActiveRecord::Base. Unfortunately, there’s no equivalent list of
extended_modules. But a quick search on Stack Overflow found an implementation of
extended_modules that we could use.
So with a bit of magic (i.e. hooks and meta-programming), we can get the lists of constituent modules from the
ActiveRecord::Base class, and include them in our
So with all that, we can now include the includable-activerecord gem and mix it in, with all the advantages that provides:
class User include ActiveRecord::Model # ... end
It was exciting to be able to make this work. Since I wrote it as a proof of concept, I haven’t written any tests yet. But it seems to be working just fine. The main thing I really need to look into is making sure that plugins that extend
ActiveRecord::Base from their own code will still work. I’m pretty sure this will work out of the box, because the
ActiveRecord::Model.included doesn’t run until the model class is loaded, and that happens after those plugins have initialized themselves.