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/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
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
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
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.
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
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
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!
Concernmodule allows us to mix in callbacks, class and instance methods, and create associations on target objects. This module has an
includedmethod, which takes a block, as well as an
class_methodsblock, 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.