In a typical Rails app, one ActiveRecord model tends to accumulate a lot of associations and related methods. This is usually the User
class; e.g., the User
has many posts, comments, contacts, projects, etc. It’s also common to have a few instance methods to filter these associations, e.g., User#unpublished_posts
, or User#recent_contacts
.
Soon this God class becomes overwhelmed by all of these responsibilities. The intent of this refactoring is to move this behavior to the associated class.
Knowing Too Much
The following ActiveRecord model has a few associations (to keep this example simple), and an instance method to filter one of them.
class User < ActiveRecord::Base
belongs_to :account
has_many :projects
has_many :contacts
def active_projects
projects.where(active: true)
end
end
User#active_projects
seems to know too much about the domain’s concept of an active project. Let’s move it to the Project
class.
Moving Responsibilities
User#active_projects
can be converted to a class method on Project
.
class Project < ActiveRecord::Base
def self.active_projects_for(user_id)
where(user_id: user_id)
.where(active: true)
end
end
We now need to update senders, e.g., ProjectsController#index
, to use this new class method.
class ProjectsController < ActionController::Base
def index
@projects = current_user.active_projects
end
end
becomes:
class ProjectsController < ActionController::Base
def index
@projects = Project.active_projects_for(current_user.id)
end
end
Responsibilities in Their Right Place
We can now remove the projects
association and related instance method from User
. User
becomes simpler, and responsibilities feel like they’re in their right place.
Replace Class Query Method with Query Object
If the associated class starts to accumulate too much behavior, or we just don’t like class methods, then we could introduce a query object.
class ActiveProjectsQuery
def initialize(relation = Project.scoped)
@relation = relation
end
def for(user_id)
@relation
.where(user_id: user_id)
.where(active: true)
end
end
Question Bidirectional Associations
When a new feature requires an ActiveRecord association, avoid the tendency to automatically make it bidirectional. Bidirectional associations often result in too many parent class responsibilities. Instead, implement these responsibilities in the associated class. It’s more work, but your classes will stay small, cohesive, and ungodlike.