loading...

A Conservative Case for Concerns

keeyan profile image Keeyan Nejad ・3 min read

There are many, many articles which condemn the use of concerns in a Rails project, and they all have valid points.
I mostly agree with the arguments, such as them causing Bi-directional dependencies and arbitrarily splitting up code into multiple files.
That said, I think a lot of these articles go too far by claiming you should never use concerns.

To be clear, if I saw a model that started with 20 concerns being included, I would be... Well, concerned.
That's why I've titles this a conservative case for concerns.
I think they should be used very sparingly, but not avoided like the Coronavirus.

Namely, I think when a concern does not depend on any specific methods or attributes of a model existing, doesn't contain code that is likely to evolve with business requirements, and could be used in a completely different project, then it's likely a valid use for a concern.

Recently, I was working on a project where we needed to import related CSV files.
In these files the associations were done based on the name of the record.
For example if we have a CSV file for articles, and each article belongs to an author, the CSV would have an author column which could have the name "Keeyan Nejad".
Then in the authors CSV we would have a name column which would include the names (assume for this example that all authors have a unique name).

When importing the CSV, the Author model would get a name from the name column.
Then I would import the articles, but when it would come time to associate an Article with an Author all I would have is the authors name, not the ID.
To solve this I would have to write something like this:

Article.create(
  title: csv_row['title'],
  content: csv_row['content'],
  author: Author.find_by(name: csv_row['author'])
)

This isn't too bad, but then I it turns out that the Authors belong to a country, and instead of having a country ID we have the name of the country.

So to import the Authors I would have to do this:

Author.create(
  name: csv_row['name'],
  country: Country.find_by(name: csv_row['country'])
)

I wanted to clean this up a bit and this is where I found a valid use for a Rails concern.
I wanted a way to associate a record by the name rather than by the ID column.

After a bit of playing around I came up with this code:

module AssociableByName
  extend ActiveSupport::Concern

  included do
    def self.associate_by_name(model)
      define_setter_for_model(model)
    end

    def self.define_setter_for_model(model)
      define_method("#{model}_name=") do |reference|
        association_class = self.class.reflect_on_association(model).klass
        association = association_class.find_by(name: reference)
        raise "Could not find #{model} by name #{reference}" if association.nil?

        send("#{model}=", association)
      end
    end
  end
end

Then in the Article model I just add these lines:

include AssociableByName
associate_by_name :author

What this code will do is create a new setter in the model called author_name= which will simply get the author ID from their name and create the association.

With that change, I can then update the importers to work more consistently:

Article.create(
  title: csv_row['title'],
  content: csv_row['content'],
  author_name: csv_row['author']
)

And that's it!
Now the article will automatically associate with the Author by the name, rather than having to do the lookup, I also get the added benefit that it will throw an error if the association doesn't exist (which is consistent with author_id if the ID didn't exist)

What do you think? I'm happy to be proven wrong and learn why this code isn't a good Concern, so let me know if you have any thoughts!

Discussion

pic
Editor guide
Collapse
jaredcwhite profile image
Jared White

To be clear, if I saw a model that started with 20 concerns being included, I would be... Well, concerned.

I'm on the pro-Concern side of the argument, so I think how I might phrase this would be: if I saw a model with 20 concerns that also had a ton of methods below that, that would be concerning. The goal of using lots of concerns is your main model file stays fairly empty, and each concern represents a logical portion of the pie that makes up the model's holistic functionality.

But anyway, your concern example here is really nice, and I'll probably swipe it on my next project. :)

Collapse
keeyan profile image
Keeyan Nejad Author

I'm glad you liked it Jared :) But I have to say, I disagree with your statement there. I think the value of a concern is to extract the super-generic stuff so that they can be used in multiple places. If you have practically no code in your model and you extract it all to concerns simply to keep your model clean you will likely end up with Concerns that depend a lot on the actual model. So they can't be reused easily and it could be harder to work out how a model works since its functionality is spread across many files

Collapse
jaredcwhite profile image
Jared White

Having some super generic stuff shared across models is handy, but that's not actually what I find concerns most useful for. :)

I think my eyes were opened specifically while watching through DHH's On Writing Software Well series, this video in particular: youtu.be/Tc5z64XIwIY

I get that not everyone thinks of organizing model code in terms of behavior sets that can be composed together, might seem odd at first. But it fits my brain at least…

Thread Thread
keeyan profile image
Keeyan Nejad Author

Thanks for sharing that Jared :) I'll definitely give it a watch