Splitting your Rails classes into modules: How does it work?
» Posted on 2011-12-01We all like the "Fat models, skinny controllers"-rule, but what to do if your models are becoming too fat? Split them up into smaller chunks, exactly. Of course this technique works for other classes, too.
The first thing to do, is to check whether the module you're looking at
should be split up into several classes. Those classes don't necessarily
have to inherit from ActiveRecord::Base, they might as well be regular
Ruby classes. For example, if you are doing a lot of radius calculations
in your Search model, you might as well add a new Radius class that
does all the hard calculation work for you and can be tested in
isolation.
But if you think that all the methods and macros belong into one class,
you can still use modules to modularize different concerns into their
own files. Doing this is easy, since we have ActiveSupport::Concern in
Rails 3.
Example:
# app/models/user/authentication.rb
class User
module Authentication
extend ActiveSupport::Concern
included do
has_secure_password
end
def create_password_reset_token
token = SecureRandom.base64.tr('+/=lIO0', 'zomglol')
self.update_attribute(:password_reset_token, token)
end
end
end
# app/models/user.rb
class User < ActiveRecord::Base
include Authentication
include Validations
# ...
end
As you can see, we have put all authentication related methods into their own module, so this module encapsulates the authentication concern.
A little explaination: Extending the module with
ActiveSupport::Concern enables the magic that makes writing modules so
much easier. The first aspect of this is the included block, that will
be run in the context of the class including the module. So it's perfect
for validations or acts_as_something style macros. You can even pass
in the base class (class including the module) if you use the inlcuded
do |base| syntax instead, if you need to.
The create_password_reset_token method gets included into the base
class, as ususal. But if you want to include class methods as well as
instance methods, you can create a separate module for the ClassMethods
inside the Authentication module. You would have to call it
ClassMethods, so that ActiveSupport::Concern can find it and do the
right thing. The ClassMethods module will be extended into the base
class, which means that these methods will act as if you wrote them with
def self.method_name.
So what are the hard parts? Of course it's naming things, and deciding
where to put stuff. First, I created a folder called user_extensions
to hold my concerns for the User class. I didn't particularly like
this, because I wanted to apply the same conventions that are valid in
gems, which is the folder should have the same name as the class. Then I
moved on to naming the folder the same as the class file and adding the
_extension suffix to each of the module files, until realizing that
this is useless. Even if there is a name clash, like an Authentication
class somewhere else, our module will be picked up first, because it
lives in the namespace of the User class. The downside of this is that
you have to address the other Authentication class (if it exists and
you need to use it in the User class) with ::Authentication, but I can
live with that, as it is a rare case. So no more name clutter, just the
stuff that is important.
Your test names and folder structure should mimic the ones of the tested
classes and concerns. So I have separate unit tests for each of the
concerns. Test class names mimic the ones of the classes, so the
User::AuthenticationTest tests the behaviour of the
User::Authentication module. And of course the test is placed in
test/unit/user/authentication_test.rb.
By the way, at the moment I am the only developer on my current project, so I enjoy the occasional developer chat. The above evolved after talking to Thorsten, Lee and Mathias. Thanks for your opinions, guys!