My Rails Models Are Bloated. Should I Use Concerns?
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
-
Put chubby models on a diet with concerns – I don’t agree with this article, but I’m including it as a counterpoint.
-
The Great Satan - Rails Concerns – “ActiveSupport::Concern actually makes your code base worse.”
-
7 Patterns to Refactor Fat ActiveRecord Models – “I discourage pulling sets of methods out of a large ActiveRecord class into ‘concerns’, or modules that are then mixed in to only one model.”
-
Why I Don’t Use ActiveSupport::Concern – “I find that they add complexity to my system without providing any value.”
Got questions? Comments? Milk?
Shoot an email to [email protected] or hit me up on Twitter (@tom_dalling).