Is a knife a good thing?
There seem to be two main camps regarding Rails concerns. One camp says concerns are good. The other camp says concerns are bad.
I don’t buy this dichotomy. To state what ought to be obvious, not everything is either good or bad.
I’ll make a small digression to illustrate my point, using the example of a knife.
You could use a knife to stab me in the leg, or you could use a knife to whittle me a magnificent wooden sculpture.
Or, if you weren’t too familiar with knives, you could try to use a knife instead of a spoon in a misguided attempt to eat a bowl of soup.
You get the idea.
I want to discuss where concerns might be a good idea and where they might not be. In order to do this it might be helpful to start by articulating which types of problems people tend to use concerns to try to solve.
The problems that people try to solve with concerns
There are two problems I’m aware of that people try to use concerns to solve.
- Models that are too big to be easily understood
- Duplication across models
Both these problems could be solved in a number of ways, including by using concerns.
(Another way could be to decompose the model object into a number of smaller plain old Ruby objects. Yet more options include service objects or Interactors. All these approaches have pros and cons and could be used appropriately or inappropriately.)
The objections some programmers have to concerns
Here are some objections to concerns that I’ve read. Some I agree with. Some I don’t.
Concerns are inheritance, and composition is better than inheritance
I totally agree with this one. An often-repeated piece of advice in OOP is “prefer composition over inheritance”. I think it’s good advice.
Having said that, the advice is not “don’t ever use inheritance!” There are a lot of cases where inheritance is more appropriate than composition, even if inheritance isn’t the first thing I reach for. In the same way, the argument that “concerns are inheritance and composition is better than inheritance” does not translate to “concerns are always bad” any more than “prefer composition over inheritance” means “inheritance is always bad”.
Concerns don’t necessarily remove dependencies, they just spread them across multiple files
I agree with this point as well. It’s totally possible to slice a model up into several concerns and end up with the result that the “after” code is actually harder to understand than the “before” code.
My response to this point would be: If you’re going to use concerns, don’t use them like that!
Concerns create circular dependencies
As far as I can reason, this is true. A well-defined inheritance relationship doesn’t create a circular dependency because the parent class doesn’t know anything about the child class. Similarly, in a well-defined composition relationship, the “finer-grained” class doesn’t know about the “coarser-grained” one.
Concerns often don’t work this way. Concerns often refer to the particulars of the models that use them.
But I would say: Why, exactly, is this sort of circular dependency bad? And if I choose not to use a concern in a particular case because I want to avoid a circular dependency, what exactly my alternative? The drawbacks of whatever I use instead of a concern aren’t automatically guaranteed not to be worse than the drawbacks of using a concern.
When code is spread across multiple files, it can be unclear where methods are defined
This argument has some merit, although a very small amount. This argument could be made equally for not just concerns but for inheritance as well. So it’s really not an argument against concerns specifically, it’s an argument against concerns and inheritance equally, or perhaps an argument against spreading code across multiple files in general.
But in any case, the problem can be easily overcome using search.
What I do when I encounter fat or duplicative models
When I encounter a fat or duplicative model, concerns are pretty much never the first solution that enters my mind.
My first instinct, again, is to consider decomposing my model into some number of plain old Ruby objects. I think sometimes Rails developers forget that plain old OOP, using plain old objects, can get you a very long way.
I often find that a large class will have one or more “hidden abstractions” inside it. There’s some sub-concept hiding in the class that doesn’t yet have a name or a cohesive bundle of code. It’s there, it’s just hiding.
In those cases I try to come up with a name for the hidden abstraction. Then I create a class named after that abstraction and move the appropriate code into my new class.
I don’t always find a hidden abstraction though. Sometimes some of the bloat is just some extra flab that neither fits neatly into the model it’s in nor makes sense as a standalone concept. This is when I (sometimes) think of using a concern.
When I use concerns
In DHH’s post about concerns he says “Concerns are also a helpful way of extracting a slice of model that doesn’t seem part of its essence” (emphasis mine). I think that’s a great way to put it.
Good use cases for concerns
To me, the perfect use case for a concern is when I have, say, 10 methods on a model that have a high level of cohesion with one another and then 2 methods that relate to each other but don’t seem to be part of the model’s essence.
If those 2 odd-man-out methods could make a new object of their own, great. I’ll do that. If not, I might consider moving them to a concern.
And if multiple models require highly symmetrical behavior (e.g. some certain file attachment behavior), I’d usually rather factor that symmetrical behavior into a concern than to an inheritance tree. And usually a concern in these cases fits much more tidily over the models than trying to create a composition relationship out of it.
Lastly, I don’t want to give the impression that I use concerns all over the place. In general when I’m programming, concerns are among the last things I reach for, not the first.
My advice concerning concerns
I think concerns are fine and, in certain scenarios, great. That of course doesn’t mean I think concerns are the best solution to every problem.
When faced with any programming problem, take stock of the tools in your toolbox and use what seems appropriate. Object composition can be good. Inheritance can be good. Concerns can be good too.