Concerns
Outdated!
This recipe is outdated. Please see newer recipes for alternatives.
Rails has had great support for Concerns
since version 3.
There is a bit of controversy around this topic. I agree that in some cases
it’s better to use composition than mixing in modules.
However in many cases where you just can’t refactor the entire app because of some constraints you are subject to, it makes perfect sense to re-organize your fat model into concerns that are both cohesive and have a single responsibility.
The nice thing about concerns is that it’s a very low risk refactor that can provide great value and clarity, and can also be a stepping stone towards a better class structure. Below I illustrate what such a refactor can look like:
A: Fat model
It is a junk drawer of methods, both ActiveRecord specific and domain specific. It has > 1,000 LOC. It is a nightmare to step into. In our example, there are methods representing 4 concerns:
- Users receive emails.
- Users have permissions.
- Users have presenters for display.
- Users have profile settings.
Each icon in the diagram represents a method from one of the concerns.
B: AR model with domain concerns mixed in
We have removed all domain specific methods into cohesive concerns. We include these concerns into the AR User model, so at runtime nothing really changes. The User model still has all the same methods. These are the concerns we extracted:
- User::ReceivesEmails
- User::HasPermissions
- User::HasPresenter
- User::HasProfileSettings
Benefits:
- It’s easy to find methods and to reason about their responsibility and how they fit into the app’s overall structure.
- You can try out different clusters of methods without affecting runtime behavior. This is a very low risk and high yield refactor.
- You can re-use behavior more easily between classes.
C: Testing a single domain concern in isolation
Now that our concern is contained in a cohesive package, we can take it and
do things with it in isolation. E.g., testing it. We just include it in a Test::User
class, stub a few dependencies, and now we can have very fast and simple
unit tests. We don’t need to satisfy everything that was part of the Fat User
model in scenario A:
- ActiveRecord validations
- ActiveRecord associations
- ActiveRecord call_backs
- state_machines
- file attachments with ImageMagick (Oh my…)
- etc.
We can test just the behavior around Permissions.
More info on Concerns
- Store concerns that belong to a resource under that resource:
E.g.,
User::HasPermissions
goes toapp/models/user/has_permissions.rb
. Concerns that cut across many resources go into/app/concerns/
: E.g.,Presenter::DateTime
. - Add the below to your auto_load_paths in application.rb (NOTE: this is not
required any more in Rails4):
config.autoload_paths += "#{ config.root }/app/concerns"
- Naming convention: If you are adding shared behavior to a polymorphic
association, call it
CommentableMixin
where:commentable
is the name of the polymorphic association. - Gotcha: Don’t use three subsequent upper case letters when naming your classes.
- Will NOT work:
IORatio
, with file namei_o_ratio.rb
. For some reason, Rails won’t load the module. - WILL work:
IoRatio
, with file nameio_ratio.rb
.
- Will NOT work:
Since Rails3 there is now a canonical way to include modules into Classes:
ActiveSupport::Concern
. Use it like so:
module User::HasPermissions
extend ActiveSupport::Concern
included do
scope :disabled, where(:disabled => true)
end
module ClassMethods
...
end
module InstanceMethods
...
end
end
Then include it in a class you want to add the behavior to:
class User < ActiveRecord::Base
include User::HasPermissions
...
end