This post is part of a series on Rails Architecture.
- The Rails Architecture Controversy
- Single Responsibility Principle and the History of Rails Architecture
- Refactoring Patterns for Display Logic
- Refactoring Patterns for Secondary Effects
- The Two Paths of Rails Architecture
We’ve talked about a number of patterns that can be added to Rails to separate out concerns from your models, views, and controllers. But what do the people guiding Rails’ development think about this? Do they encourage you to reorganize your app any way you like? Are they working to add decorators and form objects to Rails 5? Well, no, not as far as I know. Rails is gradually adding new patterns, like Active Job and Action Cable, but they’re not mainly about patterns of code organization. Instead, they’re about making apps work in entirely new ways: queues and persistent client connections, respectively.
On the topic of code organization, the message from DHH has generally been that what Rails gives you should be enough. He objects to some patterns more strenuously than others. But when it comes to code organization, I’ve heard him generally give two pieces of advice: use Rails classes the way they’re intended, and use concerns.
Concerns are effectively just mixins with a little extra Rails-specific sugar. They allow you to take some of the functionality in a class (specifically, a controller or model), put it in its own module, then include it back into the original class. Specifically, DHH argues for domain-related concerns. So instead of putting all validations in one concern, then all callbacks in another, etc., you would but everything email-related in one concern: validation, downcasing, sending emails, etc. One major advantage of concerns is that they are easy to refactor to: the model is still used in the same way, and tests should still pass.
Interestingly, both the patterns camp and the concerns camp have very similar criticisms of each other’s solution. In his conference talk about patterns for refactoring models, Bryan Helmcamp called concerns a “junk drawer.” But in a code review of a service object refactoring cited in another conference talk, DHH said that service objects usually involve “sweeping” code problems “under the rug.”
The similarity of these comments should give us pause. How is it that both sides of the argument find the same problem with each other’s solution? I think the answer is that both sides are actually solving different problems. Of course, at one level the goal is the same: writing code that is reliable and easy to work with in the shortest time possible. But the philosophies of how to get there are somewhat different.
The “patterns” group is applying classical object-oriented design principles. In classical OO, the main way to achieve the above goals is modular code. If classes are small, have a single responsibility, and are loosely coupled to other classes, then they’re easy to reason about, unit test, and reuse. Concerns, by contrast, don’t really achieve those goals: even though the source files are smaller, the model object is just as large. It has multiple responsibilities, is difficult to unit test, and there is nothing stopping the concerns in a model from all being tightly coupled to one another.
DHH clearly lays out his contrasting philosophy in his RailsConf 2014 keynote. In his view, the best way to achieve quick, reliable code is readability. As Harold Abelson famously wrote, “programs must be written for people to read, and only incidentally for machines to execute”–meaning that it’s essential for people to be able to easily understand it. In this view, breaking code up into too many different types of class means difficulty drilling through them all to figure out what a method call ultimately does. And in this view, unit tests are less important than integration tests, so the ability to test individual classes is less important.
So what are we as developers supposed to do with these two different views? Pick one side? Not entirely. Either side can be misapplied: a model can multiply concerns far beyond the point of reason, just like the patterns view can multiply classes when totally unnecessary. So it’s important to listen to the views of both sides to keep you balanced. Classical OO design is an essential foundation for where we are as an industry now—but Rails has had a huge influence on web applications as well. Both have valuable things to add to the conversation.
That being said, when your model gets too big you’re either going to create concerns, create other types of class, or both. I don’t have enough experience with either approach to be able to advocate strongly either way. Which you decide depends on things like the past experience of you and your team, and what size of projects you work on. Personally, I’m more inclined towards the well-defined approach of classical OO and unit testing, so that’s the approach I’m trying now. But I don’t feel the need to do anything as drastic as changing the Rails directory structure, or isolating my models and controllers so that my core application code doesn’t know about them. I may get there someday, but it seems better to start with the Rails defaults and get more complex only when SRP leads me to. There are so many advantages to operating within conventions that I don’t want to break them for no reason.
How about you: how have concerns or design patterns helped or hindered your apps? Let me know!