How ActiveSupport Concerns Work in Rails
This one is about ActiveSupport::Concern
from Rails. Moreover, it’s about how Ruby facilitates organizing and binding code (components/modules) together.
Ruby uses “mixin” for organizing code and sharing functionality. A “mixin” is a module that you can include in classes to add shared functionality without using inheritance. Basically, when we use include
and extend
, it’s referred to as “mixin” (most languages use inheritance hierarchies for this).
We’ll explore the ActiveSupport::Concern
module, which is prevalent in the Rails source code and it’s how Rails binds components of the framework together.
Is
ActiveSupport::Concern
too clever for its own good? That’s up to you to decide. Some programmers think that Concern hides too much magic behind a seemingly innocuous call toinclude
, and this hidden complexity carries hidden costs. Other programmers praise Concern for helping to keep Rails’ modules as slim and simple as they can be. Whatever your take onActiveSupport::Concern
, you can learn a lot by exploring its insides. – Metaprogramming Ruby 2
Aside: check out Metaprogramming Ruby book, chapter 10, if you want to go deeper into this topic. The example below are from there.
So let’s learn things about Ruby and Rails by exploring ActiveSupport::Concern
- what problem does it solve and how it came to be.
You’ve likely seen code like this in your application:
module Authorization
extend ActiveSupport::Concern
...
end
What does adding ActiveSupport::Concern
to a module do? It makes it so that including that module (also know as a “Concern”) into a target class will give the target class both instance methods and class methods from that Authorization
module.
In order to make sense of that last sentence, we need to review the difference between include
and extend
in Ruby.
The Difference Between include
and extend
Let’s say we have a module with a useful method and we want to add that method to our class:
module Hello
def self.say_hello
"hello"
end
end
We want to use say_hello
method in the Greeting
class, so we can include
the module Hello
and use it, right?
class Greeting
include Hello
end
>> Greeting.say_hello
(irb):28:in `<main>': undefined method `say_hello' for Greeting:Class (NoMethodError)
No, because say_hello
is a class method. When a class includes a module, it gets the module’s instance methods, but not the class methods.
include
mixes in the specified module’s instance methods as instance methods in the target class.extend
adds the specified module’s methods to the target class’s singleton class. And as a result, as class methods on the target class.
The above is from a previous newsletter post I wrote on the topic, check it out for more.
So what does this have to do with ActiveSupport::Concern
? Let’s look at what problem it’s solving next.
Note: The implementation of ActiveSupport::Concern
is metaprogramming heavy. It twists and bends the Ruby object model. So before we look how it’s implemented, we’ll look at how it’s used, which is more straightforward.
What Problems do “Concerns” Solve?
As we saw above, include
adds instance methods and extend
adds class methods. What if wanted a way to add both instance methods and class methods in one go?
The ActiveSupport::Concern
module provides that functionality. It also makes it easy to add that behavior into modules you define in your application. Meaning it allows you to define modules such that when you include them in a target class, that target class will get both instance and class methods from your module.
How? We can do that by adding extend ActiveSupport::Concern
to our module. That’s it. Such a module is called a “Concern”. We include Concerns into a target class to add the Concern’s instance methods and class methods. That’s a lot of words, here’s some code:
module MyConcern
extend ActiveSupport::Concern
def an_instance_method
"an instance method"
end
module ClassMethods
def a_class_method
"a class method"
end
end
end
# A class that includes the Concern
class MyClass
include MyConcern
end
# Both instance and class methods
>> MyClass.new.an_instance_method
=> "an instance method"
>> MyClass.a_class_method
=> "a class method"
Okay that’s neat. But what did Rails do before this concept of Concerns existed?
Rails Before Concern
There was this “include-and-extend” trick: when a module is included in a target class, the included
hook method is run, which essentially adds an extend
to the target class to mixin the class methods as well.
module ActiveRecord
module Validations
# Hook method
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def valid?
#...
end
end
end
end
If this feels a bit convoluted, because it is. The same effect can be achieved by adding the extend
line directly, in addition to the include
in the target class. So it is worth asking if the added complexity is worth it for removing this one line?
In addition to the complexity, this trick also had another issue with chained inclusions. It required keeping track of if a module was included as a ‘first-level’, ‘second-level’, etc. To solve this problem of chained inclusions more elegantly ActiveSupport::Concern
was crafted.
Concern Implementation: Module#append_features
The implementation of ActiverSupport::Concern
involves metaprogramming. It overrides a core Ruby method named append_features
. Inside append_features
is where module inclusion actually happens in Ruby.
For example, we can break include
if we override append_features
to do nothing:
module M
def self.append_features(base)
# Overriding to do nothing
end
def my_method
"this method will not get included since we broke append_features"
end
end
class C
include M
end
>> C.ancestors
=> [C, Object, PP::ObjectMixin, Kernel, BasicObject]
>> C.new.my_method
(irb):22:in `<main>': undefined method `my_method' for #<C:0x00007f952c615ff8> (NoMethodError)
ActiveSupport::Concern
, however, overrides append_features
and does appropriate things. That’s a glimpse of a core idea in the implementation, you can find the rest of the implementation in the rails codebase.
Before wrapping up, I want to share one last high level thought, about programming: even though I have traced this implementation and understood the concepts before, I don’t (need to) think about it on a day-to-day basis. Therefore, I don’t think I can articulate the above at the drop of a hat. The details of how ActiveRecord::Concern
is implemented are abstracted away from us as Rails developers. We can simply add extend ActiveSupport::Concern
to our modules and use the functionality. This is the power of abstraction (which can be a double-edged sword though, hidden complexity, hidden costs, etc.).
Hope you enjoyed this exploration of ActiveSupport::Concern
in Rails.
Subscribe for more Ruby, Rails, and Hotwire
Short posts, clear explanations