Concerns in Rails - good or evil?
Many consider concerns usage a good practice for Rails applications development. Others try to avoid them as much as possible. In this post we'll try to add some clarity on this controversial topic.
Why do we use Concerns
With ActiveSupport::Concern
you can extract pieces of code from your model to separate concerns(modules), and then include them back into the model.
It might look like a proper solution, because:
- You get rid of God Object
- You can put a responsibility per concern, and therefore satisfy Single Responsibility Principle
- It looks like a "composition over inheritance"
The problem is that it is not like this.
Concerns/mixins mean Multiple Inheritance
When you include a module/concern into a Class, its underlying implementation in Ruby language works very similar to inheritance:
In practice, it means that functions defined in one module can be overwritten by functions in another, and the result depends on the order of inclusion.
However, the probability of negative effects is low, and therefore this practice is considered acceptable.
But it is not the main problem. The main problem is that this approach has a fundamental flaw.
Concerns and the Modularity
Some assume that if they use concerns/modules to extract code from their models, it makes their code modular. Modularity is a different thing. Modularity means lack of circular dependencies. Dependency, in turn, means that one component knows about, or relies on the other one.
If a concern uses a method from the model, it means it depends on the model. If the model, which includes the concern, uses a method defined in the concern, it depends on the concern. This is how you introduce circular dependencies into your code!
If you don't have any rules on what is okay to extract to concerns, then where you previously head a God Object, you get a complex non-modular code, which gets assembld into the God Object again during runtime.
The non-modular code is harder to test and harder to modify. You simply can't change one component without touching the other one.
It would be fair to say that concerns don't solve the fundamental problem, but rather play a cosmetic role. They make your code look nicer than it is.
Concerns and Code Climate
The thing is that if you use CodeClimate or a similar tool to measure the quality of your code, they could make things even worse.
If you take a look at what things CodeClimate cares about, you find nothing about dependencies:
It means that when you extract code into separate concerns, your metrics start to look better, while fundamentally you code stays the same or becomes worse because of intertangled dependencies.
Such metrics makes us focus on unimportant, or at least secondary things, like the length of functions, classes etc. If we focus on wrong metrics, we make wrong decisions :(
Concerns and Dependency Inversion Principle
In Dependency Inversion Principle there's an idea that low-level components can depend on abstarctions. Some might assume that concerns are exactly this kind of abstractions, and it is fine to depend on them.
Well, it could be the case! For example, if you depend on Comparable
or Enumerable
, it is totally legit!
But here's the list of concerns I've seen in one Rails app:
Markdownable
CreditCardable
DeviseControllable
Geocodable
These things are not abstractions! They contain very concrete implementations inside (and they, of course, rely on stuff from the User
model). If this is the way you use concerns, then one day you'll be in big troubles.
Concerns and Modeling
Of course, there could be different approaches. The problem with the approach of behavior inside models is that the bigger your app gets, the more behavior you have to put into your models. You simply leave yourself no choice.
How to attack the problem? Well, it is the huge topic and goes far beyond the scope of this post.
But I'll give you one hint:
This idea comes from the misunderstanding of the term Modeling. Modeling means the process of definition of the ontology of your system.
Ontology contains concepts, their attributes and relationships between them. The behavior of the system is not included, and should be separated from your Model layer.
Wrapping up
- Concerns on their own are not evil
- They become evil if you use them without any limitations
- You have to be disciplined to prevent its negative effect
- Discipline is hard, therefore it is better to avoid using Concerns as First Class Citizens.
In general, you should treat them the same way we treat meta-programming. It is fine to use them to construct underlying building blocks of our system, but don't let them spoil your Models.