Words and Code

One writer’s journey from words to code.

Stop Worrying and Start Being Concerned: ActiveSupport Concerns

#technicaltuesdays, rails, ruby


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
class User < ActiveRecord::Base
  after_commit :register_user, on: :create

  def register_user
      # Where our logic for registering a user
      # would go. Would call on a background job
      # to perform and send our registration email.
  end
end

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
module Users
  module Registerer
      extend ActiveSupport::Concern
  end
end

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
module Users
  module Registerer
      extend ActiveSupport::Concern

      included do
          has_one :registration, dependent: :destroy

          after_commit :register_user, on: :create
      end

      def register_user
          send_registration_email(self)
          
          touch(:registered_at)
      end

      def send_registration_email(self)
          RegistrationEmailerJob.perform_later(self)
      end
  end
end

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
class User < ActiveRecord::Base
  include Users::Registerer
end

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
class Organization < ActiveRecord::Base
  include Users::Registerer
end

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
class CommentsController < ApplicationController
  skip_after_action :verify_authorized
  skip_after_action :verify_policy_scoped

  # RESTful controller actions go here!
end

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
module SkipAuthorized
  extend ActiveSupport::Concern

  included do
    skip_after_action :verify_authorized
    skip_after_action :verify_policy_scoped
  end
end

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
class CommentsController < ApplicationController
  include SkipAuthorized

  # RESTful controller actions go here!
end

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 an included method, which takes a block, as well as an append_features method and class_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.