Recognizing MVC Myopia in a Rails Application
How did we get here?
Stop me if you’ve heard this one: “Don’t put logic in your views”. Although Rails developers debate how much logic is too much for a view, most have embraced the idea. If you dare to peak into some Wordpress code you can see the alternative. The HTML to render the login page is mixed with ideas about how users are authenticated and how password reset emails work. There is a certain type of cohesion to this approach (“all the login stuff”) but the benefits of using an MVC architecture seem often outweigh the benefits of this cohesion.
If you shouldn’t put logic in your Rails views, then where can it go? Rails Helpers give developers a nice place to put methods that will format strings or spit out snippets of HTML, but they don’t accommodate the code that embodies the business logic and processes of an application.
The next obvious destination for application code is Rails controllers. If
you’ve never seen a controller’s
create method more than fill your text
editor’s screen, then you’ve either been very lucky or haven’t worked with many
Rails applications. The trouble with controllers is that there is almost no
limit to what logic can live in them. We needed a mantra to save us from those
1000 line controller classes: Skinny Controller, Fat
appeared on the scene.
“No logic in your views” and “fat controller, skinny model” will serve you well if you’re talking about database queries. That’s because models are well-suited to perform queries. But what about everything else?
- Complicated user authentication
- User authorization (e.g. who can delete this post?)
- Multi-model creation and validation with side effects
- Complex error handling when interacting with 3rd party services
The biggest problems I see in Rails application design happen because developers think in terms of 3 class types - Views, Controllers, and Models (ActiveRecord models to be precise). Soon they’re backed into a corner and start exhibiting the following symptoms:
- They put logic into ActiveRecord callbacks that handle different use
cases by passing in control flags:
post.save(notify: false, loose_validation: true)
- They extract logic into modules (see also “concerns” or “helpers”) in an attempt to codify ideas and make their model classes smaller
- They use observers (deprecated in Rails 4.0) to decouple their model events from other logic so that they can test parts of their application without stubbing the world
- They heavily favor gems that hook into Rails models and controllers over writing their own code to implement trivial features
- They give up on logicless views and start conditionally rendering HTML based on intricate checks of model state
I have done all of these things and I see these issues in almost every Rails application I work on.
What to do?
The journey through Rails is laden with traps (Concerns, Single Table Inheritance, Accepts Nested Attributes) and it’s creator has some pretty obnoxious stances on TDD and some OO design guidelines. Still I’m not ready to give up on Rails yet. Learning to pull your domain away from any framework is a necessity for non-trivial applications.
The best article to start with for a quick win is 7 Ways to Refactor Fat Models. Specifically Value Objects, Service Objects, Policy Objects and Decorators are patterns that I use everyday when coding.
If you really want to drink from the fire hose checkout 40+ Resources For Building Robust Rails Applications. And of course there is always Growing Object-Oriented Software Guided By Tests.