Do you have giant models in your Rails app? We're sharing a few practical ways to put giant models on a diet, starting with — Model-Specific Concern.

Problem: Do you have giant models in Rails app?

Your team just can't stop adding new code into these giant models.

You feel the new code belongs to this model and it's convenient to just add it. Both the model and its unit-test file continue to grow in size over time. Now you feel uneasy but there seems to be no better way.

Why do we want to avoid giant models?

  1. Giant model files are difficult to understand and change.
  2. Huge unit-test files are difficult to navigate, and harder to split for parallel testing.

We practice a few pragmatic patterns to put giant models on a diet. These patterns are convenient and easy to adopt by any team, and have been adopted by a number of teams. You can use them too!

In this post, we'll review the first pattern: Model-Specific Concern.

Solution: Model-Specific Concern pattern

The Concern pattern was popularized by the Basecamp team. It was added into the Rails framework as a way to share cross-cutting concerns among multiple models.

Besides sharing across models, what if you apply a concern to only a single specific model?

For example; You know that User is a giant model, but you just can't resist adding this method directly to the model file, it's just too convenient:

# app/models/user.rb
class User < ApplicationRecord
  searchkick(
    text_start: [
      :name, :company_name
    ],
    suggest: [:name],
    searchable: [
      :name, :email, :phone,
      :mobile, :company_name
    ],
    callbacks: true
  )
  
  def self.search, ->(term) do
    ...
  end
  
  def search_data
    {
      :name, :email, :phone,
      :mobile, :company_name
    }
  end
end

# spec/models/user_spec.rb
RSpec.describe User do
  ...
  describe ".search" do
    ...
    expect(User.search(name).to ...
  end
end

Does this situation look familiar?

Let's see how you can avoid adding this method into the giant User model.

Using model-specific concerns

First, we create a model-specific concern for User, adding a new file:

# app/models/user/searchable.rb
module User::Searchable
  extend ActiveSupport::Concern
  
  included do
    searchkick(
      text_start: [
        :name, :company_name
      ],
      suggest: [:name],
      searchable: [
        :name, :email, :phone,
        :mobile, :company_name
      ],
      callbacks: true
    )
      
    def self.search, ->(term) do
      ...
    end
    
    def search_data
      {
        :name, :email, :phone,
        :mobile, :company_name
      }
    end
  end
end

Then, include this concern into the User model:

# app/models/user.rb
class User < ApplicationRecord
  include Searchable
  ...
end

And now, we can write a unit-test in a new, dedicated, spec file:

# spec/models/user/searchable_spec.rb
RSpec.describe User::Searchable do
  describe ".search" do
    ...
    expect(User.search(name).to ...
  end
end

The same pattern can be applied when adding callbacks and other functionality.

Effective refactoring tool

After we first saw this approach used by Basecamp, we applied it to address giant model issues in a big enterprise Rails app.

We used it to immediately stop new code going into the giant models and their huge unit-test files. Then, continued to apply it as a refactoring tool to extract out code from the models.

Because of their simplicity and convenience, model-specific concerns are easy to adopt as a team.

Model-specific concerns can also be a refactoring stepping-stone, before deciding which more-involved patterns to apply. We'll cover those patterns next in the blog series. Subscribe our Twitter account to get notified.