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.
Classical Inheritance
When you use ActiveRecord, you normally include it in your model like this:
class User < ActiveRecord::Base # ... end
Your 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 ActiveRecord::Base.
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 Base
.
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 ActiveRecord::Base
or Person
. 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 Person
from ActiveRecord::Base
and then subclassing User
from 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.
Mixins
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.
Implementation
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 ActiveRecord::Model
module.
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.