We 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.
# 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 is 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.
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
So what are the hard parts? Of course it is 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 did not 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
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!