A few weeks ago, while learning everything I never knew about user authorization, I also stumbled upon a cool refactoring pattern that I didn’t even know existed. This pattern is based on the simple idea of Ruby modules and mixins, but is particularly handy when it comes to dealing with class methods and callbacks.
ActiveSupport is a pretty massive component within Rails, and it’s responsible for a ton of different functionality, including language extensions and utilities. I last wrote about ActiveSupport back when we were exploring the Rails inflector and the libraries it provides for handling the pluralization of different strings. This was way back in September, and at the time, my understanding of ActiveSupport was pretty limited. It turns out that yes, ActiveSupport does provide a bunch of different patterns to transform simple Ruby strings…but it also has a lot more going on inside of it. For example, the ActiveSupport Concern module, which only recently made its debut in Rails 4.
The ActiveSupport::Concern wrapper is an interesting way of encapsulating…well, certain functionality that you might be concerned with. These concerns take advantage of two directories that are automatically part of the load path within a Rails application: app/models/concerns
and app/controllers/concerns
. So, how do you write a concern, and what should go inside of it? Don’t worry, that’s exactly what we’ll concern ourselves with next.
Should We Be Concerned?
Concerns are meant to make our lives less complicated. Or at the very least, we should be less concerned about the quality of our code if we use concerns, right? But what are ActiveSupport’s Concerns really meant to be used for? And how do we know if we should be using them? Well, to answer this question, we can turn to the creator of Rails himself. In a blog post pre-Rails 4 titled Put chubby models on a diet with concerns, DHH explains when and why to consider using ActiveSupport’s Concern module:
“Concerns encapsulate both data access and domain logic about a certain slice of responsibility. Concerns are also a helpful way of extracting a slice of model that doesn’t seem part of its essence (what is and isn’t in the essence of a model is a fuzzy line and a longer discussion) without going full-bore Single Responsibility Principle and running the risk of ballooning your object inventory.”
When we talk about concerns, what we really are honing in on is the most effective separation of concerns. I really like the way that David thinks of models having an “essence”, and I think that this is a great way of approaching when and when not to use a concern.
Let’s look at our bookstore application. We have an User
model for anyone that signs up to use our application. Whenever a User
signs up, we want to send them an email telling them that they’ve been registered, and probably highlighting some of the cool things that they can do to set up their profile on our application. Now, this seems like something that only the User
model would be concerned with, right? Well, yes, until we realize that we have another model that needs to share this same functionality!
For example, we now have organizations that want to sign up for our application. They also need to receive the same email and be “registered”. As our application grows, we might even want to create a Registration
model, which would belong to an User
and Organization
. Now, obviously we could accomplish what we wanted to by just adding the same lines of code to both models, but that makes for neither DRY code, nor a great separation of concerns. But wouldn’t it be great if we could take this piece of “registration” functionality, wrap it up, and only pull it out when we need to use it? It turns out that is exactly what we can do with ActiveSupport::Concern
.
Extending Our Concerns
Before we write our concern, let’s look at what our User
model looks like. Here’s a truncated version that contains only the logic pertaining to registering a user:
1 2 3 4 5 6 7 8 9 |
|
We very well could stick this inside of our organization.rb
model file, but there’s a better way to do this. There are a few steps to creating a concern, the first of which is recognizing where to put it! Since we’re creating a concern for a model, this will live inside of our app/models/concerns
directory. We’ll call this concern a Registerer
concern, since that’s its single responsibility, and we can preemptively namespace our concern under Users
, which would make its path app/models/concerns/users/registerer.rb
.
Next, we’ll want to extend
the Rails ActiveSupport::Concern
module itself from within our concern:
1 2 3 4 5 |
|
Now, for the actual writing, there’s one method that’s going to be our new best friend: the included
method, which takes a block. A little-known fact about this callback is that it’s actually defined by Ruby’s Module
class, and it’s called whenever a module is “included” into another class or module. This is where we’ll put the important class methods and callbacks that we want to be shared amongst the models that will use our concern.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
This is mostly straightforward. All the logic for a registration now lives in this single file, including the creating of a registration association on our target object (in this case, the User
model), the registering of a user by passing our User
instance (self
) to our RegistrationEmailerJob
, and the updating of the registered_at
attribute on our User
model using the touch method — assuming, of course, that we’ve defensively coded this attribute onto our User
model. We’re also able to use the after_commit
callback hook, since the included
method can accept callback names as parameters.
Now that we have all this code in one place, how do we add it to our model? Well, we can do it in a single line:
1 2 3 |
|
All we need to do is include
our concern, just as we would a module. And down the road, when we find out that we need to create Organization
model that shares this set of functionality, all we need to do is add the exact same line to our new model:
1 2 3 |
|
And here’s the really nice part about utilizing concerns in this way: when we realize that we need to change how this works — maybe we need to add another job or service object, or perhaps another, more specific callback — we can add it to one place and update our logic in a single file! This ties in quite nicely to DHH’s point of a model’s “essence”. In this case, being able to be “registered” isn’t necessarily something that pertains to the User
model specifically. But, it also doesn’t need to be its own object per se. Instead, we really just need a set of methods that can be available to be invoked upon an object, which is exactly what ActiveSupport::Concern
provides us with.
Helpful Inclusion
Because concerns are so simple to extend and include, there are lots of use cases for them. We learned earlier that Rails comes with two concerns
directories preloaded: one for models
, and another for controllers
. Let’s look at a practical example for using ActiveSupport::Concern
in the context of a controller.
We recently added the pundit
gem to our bookstore app for user authorization. But we only had a few controller actions that actually needed to be authorized; the rest of our controllers didn’t need any authorization, because they could be accessed by anyone. Our Reviews
controller was being authorized, for example, but our Comments
controller didn’t need any authorization whatsoever.
So, for the controllers that didn’t need authorization, what did we do? Well, we were adding some skip_after_actions
lines, which were instructions that the pundit
gem documentation had given us:
1 2 3 4 5 6 |
|
Now, imagine we also have a BlogsController
with just an index
action API endpoint, which doesn’t need to be authorized. And maybe we also have TagsController
, which also doesn’t need to be authorized by pundit
. We could copy and paste these two lines into every single controller…or, we could use our newfound knowledge of ActiveSupport::Concern
!
Let’s share some of this code, shall we? We can create a skip_authorized.rb
file inside of app/controllers/concerns
. And inside of it, we’ll include Pundit
— otherwise, our skip_after_actions
will have no idea what actions we’re trying to skip! Our concern might look something like this:
1 2 3 4 5 6 7 8 |
|
Pretty simple, right? And suddenly, our CommentsController
, BlogsController
, TagsController
, and pretty much every single controller that we want to share these skip_after_action
callbacks now can be refactored to have this single line:
1 2 3 4 5 |
|
And now, if we wanted to rescue
from a Pundit::UnauthorizedError
, we could add a single line, into a single file…but all of our controllers would mix that in! Similarly, we could create an Authorized
concern for every controller that needed to actually implement pundit
authorization. See, there’s no need to worry for the rest of our days, because instead, we can just be concerned — ActiveSupport concerned!
tl;dr?
- ActiveSupport’s
Concern
module allows us to mix in callbacks, class and instance methods, and create associations on target objects. This module has anincluded
method, which takes a block, as well as anappend_features
method andclass_methods
block, which you can read about in the source code. - This blog post is pretty fantastic in its explanation of mixins, modules, and concerns.
- Concerns are a little controversial in Railsland. This slide deck from RailsConf 2014 shares a bit about why that’s the case.