Splitting your Rails classes into modules: How does it work?

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.

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 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.

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 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 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!

Show Comments