When you read Rails blogs and books, or watch conference talks, you’ll learn a lot about making your models skinnier.
These techniques are awesome, because your models will get too big or complex to handle. But do you really want to go as far as having your models only responsible for persistence, associations, and validations? How do you decide how much logic should stay in your ActiveRecord models, anyway?
Skinny. But not too skinny.
Active Record is a pattern that works best when your models closely match your database schema. That’s what it’s designed for! But what does that mean?
-
If some code does work from the point of view of an ActiveRecord model, it can go into the model.
-
If some code does work that spans multiple tables/objects, and doesn’t really have a clear owner, it could go into a Service Object.
-
Anything that’s attribute-like (like attributes calculated from associations or other attributes) should go into your ActiveRecord model.
-
If you have logic that has to orchestrate the saving or updating of multiple models at once, it should go into an ActiveModel Form Object.
-
If code is mostly meant for displaying or formatting models in a view, it should go into a Rails helper or a Presenter.
Beyond those guidelines, you should use the same rules you’d use to refactor any class that’s getting too big. But generally, you shouldn’t feel bad about leaving some logic in your ActiveRecord models. They’re meant to have it!
So, since Rails chose the Active Record pattern, it makes sense to have some logic in your models. But why did Rails pick that pattern instead of something cleaner?
What Would Ruby Do?
In Patterns of Enterprise Application Architecture, the Active Record pattern holds the middle ground between Row Data Gateway and Data Mapper. Row Data Gateway is a mostly dumb object-oriented wrapper around table rows, like the skinniest of all possible models. Data Mappers are more complex than Active Records, and are mostly used to convert between objects containing nothing but business logic and objects containing nothing but persistence logic.
So, in the context of Ruby, Active Record is absolutely the correct default pattern.
Why do I say that?
Ruby is designed to make programmers happy. When it’s forced to make tradeoffs between cleanliness and convenience, it almost always chooses convenience.
I mean, Array
has over a hundred methods in its public API. It has a ton of methods that are just aliases of one another. Because some developers just prefer writing .detect
to .find
.
In that context, it makes a lot of sense that Rails would default to a pattern that’s convenient over one that’s more flexible, or has more object-oriented purity. It’s the Ruby way. And I love it.
You can always refactor to something more flexible when you need to. But then again, YAGNI.