Concerning Yourself with ActiveSupport::Concern

Picking up on our last talk about working with the Rails core, I wanted to take some time to introduce you to the internals of Rails 3, in hopes to break down any fears about hacking around in Rails. We are going to talk today about ActiveSupport::Concern.

Preface

For those of you new to ActiveSupport, let’s take a step back. Have you ever worked on a codebase and had to constantly do some utility work, like encoding or decoding JSON, generate a random number, or encrypt data? Of course, we all have, and the Rails core developers are no different. ActiveSupport is a library of such utilities that you are free to use not only in Rails, but in your own standalone Ruby project! I feel that ActiveSupport is the best place to start learning Rails core code: in core libraries, it takes multiple classes and modules to see a functionality come to life. Compounded by your unfamiliarity with the idioms that the core developers use, you may quickly find yourself getting lost. In ActiveSupport, most of the modules standalone, so you can look at the one file to find out what it’s doing (many helper modules are < 200 lines of code with comments). Also, it gives you small doses of such idioms, so you can get comfortable with core design patterns before diving into heavy lifting code.

::Concern

So now that you know about ActiveSupport, you won’t be surprised to learn that ActiveSupport::Concern is just another helpful utility module. But you might not understand what it’s useful for until you learn a little bit about a common Rails metaprogramming design pattern.

Mixing Class and Instance methods into your classes

Commonly in Rails, we use 3rd-party gems to add certain functionality to our classes; most commonly of which is ActiveRecord::Base. These gems usually add methods to instances of AR::Base, and to the class itself. For example, a tagging library might add an instance method @blog.tags, and a class method Blog.find_by_tags. If you’ve never looked under the hood to see how libraries do this, it may be look a little roundabout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module TagLib
 
  module ClassMethods
    def find_by_tags()
      # ...
    end
  end
 
  module InstanceMethods
    def tags()
      # ...
    end
  end
 
  def self.included(base)
    base.send :include, InstanceMethods
    base.send :extend, ClassMethods
  end
 
end

class ActiveRecord::Base
  include TagLib
end

This is a common Rails idiom that many developers are used to seeing. The system breaks down like this:

  1. Start on line 1, where we create a module, ours is TagLib, which will provide tagging functionality
  2. On line 23, we open up ActiveRecord::Base (AR::Base) and include our module. This will make the TagLib library available to all AR::Base classes. Note: this code that opens AR::Base is not inside of our module
  3. On line 15, we override the
    1
    self.included()

    method.

    1
    self.included()

    is a special “callback” method that gets automatically called when ever the module is included into something. In our case, we included TagLib into AR::Base, so this method will be called. self.included() takes a parameter, which is a reference to the class that included in it, in our case AR::Base. We can now take that reference, and use it to add methods to it. Note: we use

    1
    base.send :extend

    , instead of the basic

    1
    extend

    , to get around private method hiding.

  4. By calling self.included() in your class, it will include all of the instance methods of the class with the methods in the InstanceMethods module, giving your @blog.tags, and it will add all of the methods in the ClassMethods module to the class, so you can do Blog.find_by_tags.

There are many reasons why this system is a little bit hacky, and more will become apparent when you dive deepy into the Ruby language and metaprogramming. A couple standouts are:

  • You are overriding the
    1
    self.included()

    method to act like an extend method

  • When you include a method into a class, the methods automatically become apart of all instances in the class. It is not always necessary to have an InstanceMethods module to include another module.
  • It’s not readable, people have to struggle just to figure out this whole bootstrapping process.

Note: Smarter people have explained this way better than me

ActiveSupport::Concern to the rescue

Looking for a way to keep the same design pattern, but abstract the complexities out of the code, Josh Peek wrote ActiveSupport::Concern, which allows you to pull off our same TagLib module by doing something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module TagLib
  extend ActiveSupport::Concern

  module ClassMethods
    def find_by_tags()
      # ...
    end
  end
 
  module InstanceMethods
    def tags()
      # ...
    end
  end
end

class ActiveRecord::Base
  include TagLib
end

AS::Concern will look for modules named ClassMethods and InstanceMethods and bootstrap them as you normally would like. The module has some other nice benefits, for instance, sometimes you want to add other code in the

1
self.included()

method, such as logging:

1
2
3
def self.included(base)
  logger.warn("Adding TabLib - this will make you class awesome. Proceed with awesomeness")
  #...

Since AS::Concern removes the need for that call, it also provides you with an

1
included()

method that takes a block, so now you can do:

1
2
3
included do
  logger.warn ...
end

Hopefully, you’ve gotten a taste of some Rails idioms, Rails core code, and an explanation of a core module. I assure you, there are many more in ActiveSupport that are equally as straightforward to follow, so get hacking! (check out GZip, Buffered Logger, and Message Encryptor)

  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Twitter

Comment | Trackback

Uncategorized

11 Responses to “Concerning Yourself with ActiveSupport::Concern”

  1. Aaron Godin Says:

    April 29th, 2011 at 8:27 pm

    Thanks for the post! I’m looking at constructing my first gem to use with ActiveRecord, and this helps me get through the tough parts. Now I can go crack open my favorite gems.

  2. Frank Says:

    April 30th, 2011 at 4:47 am

    I’m glad it helped you out

  3. Jaime Iniesta Says:

    May 24th, 2011 at 9:46 am

    Thanks for the post!

    There’s a little typo (twice): “TabLib” instead of “TagLib”

  4. Frank Says:

    May 24th, 2011 at 9:50 am

    Glad you liked it, all fixed

  5. Charles Feduke Says:

    May 26th, 2011 at 10:35 am

    This is useful to know. I just wrote some code where I could have used ActiveSupport::Concern yesterday (in place of the old way). Even though you’re not losing very many LOC the purpose becomes a bit clearer and more readable.

  6. Frank Says:

    May 26th, 2011 at 10:39 am

    You’re absolutely right, Charles. The whole “Rails 3 gut” was all about cleaner, more compartmentalized code. Making this mixin process a little more auto-magic makes it clearer by standardizing a common bootstrap process that many have been hacking about.

  7. Giang Nguyen Says:

    August 29th, 2011 at 12:23 pm

    Nice article.

    Suppose I have a Tag model that corresponds to a Tags table in database, in Tag model I include TagLib. Also, I put some logger messages in include callback. I notice that every time I do something with a Tag instance that calls methods in TagLib, the logger messages show up in log.

    That means everytime a Tag instance is accessed, the TagLib gets included again?

    If so, is there any other efficient way to get rid of including TagLib methods every time Tag instance is accessed?

    Thanks,
    -Giang

  8. Mark Says:

    October 10th, 2011 at 11:50 pm

    I had to debug someone else’s code today, and it was only able to understand it after I had read this post. Thanks!

  9. Dhiren Gupta Says:

    November 22nd, 2011 at 2:59 am

    Thanks for the post !!. It’s very helpful.

  10. Kevin Triplett Says:

    March 9th, 2012 at 1:37 am

    The latest Rails (3.2 I believe) is deprecating the InstanceMethods. As the deprecation warning explains, just include instance methods in the module. So the module would be:

    module TagLib
    extend ActiveSupport::Concern

    module ClassMethods
    def find_by_tags()
    # …
    end
    end

    def tags()
    # …
    end
    end

  11. apeiros Says:

    May 2nd, 2012 at 2:47 am

    extend is not private, only include is.

Leave a Reply