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!