My Rails Models Are Bloated. Should I Use Concerns?

??? words · ??? min read

tl;dr Probably not. It’ll just make the code worse.

The typical scenario goes something like this:

In the beginning, you implement new functionality by adding methods to your model classes. That’s what DHH does, so it can’t be that bad. And it works fine… for a while.

Your web app is a lot larger now, and the model classes are growing out of control. The User model is especially big. The app/models/user.rb file is thousands of lines long. You dread making changes to the larger model classes because they are hard to understand, and easy to break.

You decide it’s time to refactor those painful model classes. You look at what Rails provides, and find only one tool for the job: ActiveSupport::Concern.

The word “concern” has other meanings in the context of programming, but when I use it in this article I’m referring to ActiveSupport::Concern. It’s unfortunate that Rails chose to coopt the word.

Concerns look good. Functionality will get extracted out into smaller modules, making the model classes smaller too. That’s the goal: to turn one big complicated piece of code into smaller, simpler pieces of code.

So, will concerns make your model classes simpler, and easier to work with? Probably not.

What Actually Happens

You’ve taken your big model class and extracted functionality out into a few concerns. The model class file used to be thousands of lines long, but now it’s only a few hundred lines long. Each concern is a couple of hundred lines long too. Problem solved!

Later, however, you find that working with the model class isn’t any easier. In fact it’s worse. All the complicated dependencies and interactions still exist, but now they are spread across multiple files, making them even harder to understand.

The end result is the opposite of what you intended. The model class has become more complicated.

Small Files != Simple Code

Shouldn’t small things be simpler than big things? It’s true that the files have fewer lines of code in them, but file size isn’t the thing that you’re trying to reduce. If it was, we could magically solve a bunch of problems by putting each method into a separate file.

What you really want are smaller classes – fewer responsibilities, fewer dependencies, less coupling, etc. When viewed from this angle, the model class is just as big as it was before the refactoring.

Concerns, and mixins in general, are poor choices for breaking down a large class. They are not self-contained – they pollute the classes that they are included into. They create cyclic dependencies: the mixin depends on methods of the class, and the class depends on the mixin methods. Remember that mixins are essentially multiple inheritance.

What Mixins Are Actually For

If you look at Ruby core and the standard library, you’ll notice that mixins are used for one thing: convenience methods. Take Comparable, for example. It allows you to write x < y, which could otherwise be written as (x <=> y) < 0. The < method makes code nicer to read, but it’s just piggybacking off the functionality of the <=> method, which the Comparable depends on. The same can be said for Enumerable and Forwardable.

When it comes to Rails, it’s a contentious issue whether concerns should be used at all. Controversy aside, slimming down a single model is still probably a bad use case for concerns. Concerns are used to give identical functionality to multiple different model classes. For example, if you can apply tags to both users and blog posts, then you might make a Taggable concern that gets included into the User and BlogPost model classes. A concern that is only included into a single model class is a kind of code smell.

Alternatives

Prefer composition over inheritance (mixins are inheritance). It’s better to create separate, decoupled classes. There are several different types of classes you can create, depending on the type of functionality being extracted. Have a look at this article on the Code Climate blog about extracting:

  • Value objects
  • Service objects
  • Form objects
  • Query objects
  • View objects
  • Policy objects
  • Decorators

Proper decomposition tends to involve POROs: Plain Old Ruby Objects. All of the types of objects listed above could be implemented as POROs.

Also have a look at the new Attributes API in Rails 5. If you’re extracting methods for getting and setting model attributes, this might be what you want. Here is an example from the documentation that shows converting between money strings and integers:

class MoneyType < ActiveRecord::Type::Integer
  def cast(value)
    if !value.kind_of?(Numeric) && value.include?('$')
      price_in_dollars = value.gsub(/\$/, '').to_f
      super(price_in_dollars * 100)
    else
      super
    end
  end
end

# config/initializers/types.rb
ActiveRecord::Type.register(:money, MoneyType)

# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, :money
end

store_listing = StoreListing.new(price_in_cents: '$10.00')
store_listing.price_in_cents # => 1000

Further Reading

Got questions? Comments? Milk?

Shoot an email to [email protected] or hit me up on Twitter (@tom_dalling).

← Previously: RSpec Expectations Cheat Sheet

Next up: The Pure Function As An Object (PFAAO) Pattern →

Join The Pigeonhole

Don't miss the next post! Subscribe to Ruby Pigeon mailing list and get the next post sent straight to your inbox.