Why use ActiveSupport::Concern instead of just a plain module? - ruby-on-rails

I never understood why one has to use ActiveSupport::Concern is used for mixins instead of a plain module. Is there a simple answer to what ActiveSupport::Concern provides (at least in Rails 5) that a simple module without using ActiveSupport::Concern will do?

From https://api.rubyonrails.org/classes/ActiveSupport/Concern.html:
A typical module looks like this:
module M
def self.included(base)
base.extend ClassMethods
base.class_eval do
scope :disabled, -> { where(disabled: true) }
end
end
module ClassMethods
...
end
end
By using ActiveSupport::Concern the above module could instead be written as:
require 'active_support/concern'
module M
extend ActiveSupport::Concern
included do
scope :disabled, -> { where(disabled: true) }
end
class_methods do
...
end
end
Moreover, it gracefully handles module dependencies. Given a Foo module and a Bar module which depends on the former, we would typically write the following:
module Foo
def self.included(base)
base.class_eval do
def self.method_injected_by_foo
...
end
end
end
end
module Bar
def self.included(base)
base.method_injected_by_foo
end
end
class Host
include Foo # We need to include this dependency for Bar
include Bar # Bar is the module that Host really needs
end
But why should Host care about Bar's dependencies, namely Foo? We could try to hide these from Host directly including Foo in Bar:
module Bar
include Foo
def self.included(base)
base.method_injected_by_foo
end
end
class Host
include Bar
end
Unfortunately this won't work, since when Foo is included, its base is the Bar module, not the Host class. With ActiveSupport::Concern, module dependencies are properly resolved:
require 'active_support/concern'
module Foo
extend ActiveSupport::Concern
included do
def self.method_injected_by_foo
...
end
end
end
module Bar
extend ActiveSupport::Concern
include Foo
included do
self.method_injected_by_foo
end
end
class Host
include Bar # It works, now Bar takes care of its dependencies
end

Related

What is Ruby module.included?

I'm trying to better my understanding of meta-programming in Ruby and am confused as to what Module.included is? My current understanding is that this is a callback invoked by Ruby whenever the module is included into another module or class. Other than that, what types of (meta-)programming constructs are these used in? Any examples?
Module#included allows modules to inject both class and instance methods, and execute associated code, from a single include.
The documentation for ActiveSupport::Concern illustrates a typical use. It's injecting class methods into the calling class, and executing code. In this case, adding a scope.
module M
def self.included(base)
base.extend ClassMethods
base.class_eval do
scope :disabled, -> { where(disabled: true) }
end
end
module ClassMethods
...
end
end
And here's the ActiveSupport::Concern version which does the same thing, but adds declarative syntax sugar.
require 'active_support/concern'
module M
extend ActiveSupport::Concern
included do
scope :disabled, -> { where(disabled: true) }
end
class_methods do
...
end
end
With included a class simply includes the module. It allows the module to be one neat package: instance methods, class methods, and setup code.
class Thing
# Class and instance methods are injected, and the new scope is added.
include M
end
Without included a module can only inject instance methods. Class methods would have to be added separately, as well as executing any setup code.
module M
def some_instance_method
...
end
module ClassMethods
def setup
scope :disabled, -> { where(disabled: true) }
end
end
end
class Thing
# Inject the instance methods
include M
# Inject the class methods
extend M::ClassMethods
# Run any setup code.
setup
end
Other examples might be registering a class, for example as an available plugin.
module Plugin
def self.included(base)
base.extend ClassMethods
base.class_eval do
register_as_plugin(base)
end
end
module ClassMethods
def register_as_plugin(klass)
...
end
end
end
class Thing
include Plugin
end
Or adding necessary accessors.
module HasLogger
def self.included(base)
base.class_eval do
attr_writer :logger
end
end
def logger
#logger ||= Rails.logger
end
end
class Thing
include HasLogger
end

how to find out in which class i currently include my module?

how do we find out in which class we are currently including a module? (coming from rails-background where we do the has_* style modules)
class Foo
has_likes
end
module HasLikes
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def has_likes(options = {})
end
end
module ContentMethods
def self.included(base)
base.extend ClassMethods
end
# ?????
# how do we get Foo here?
end
end
First of all, as #Jordan said in comments, it smells as a design flaw.
Secondary, inside ContentMethods declaration, it’s definitely impossible, since it is always included after been declared.
In general, one might do it afterwards with ObjectSpace#each_object:
module HasLikes
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def has_likes(options = {})
end
end
module ContentMethods
def self.included(base)
base.extend ClassMethods
end
end
end
class Foo
include ::HasLikes::ContentMethods
has_likes
end
ObjectSpace.each_object(Class) do |x|
p(x) if x <= ::HasLikes::ContentMethods
end
#⇒ Foo
But please do not do that at home or school.

What does class_methods do in concerns?

I am reading some codes which use the concerns in Rails 4.
I read some articles to say, if we would like to include class methods
using module ClassMethods, but the code I read using something like:
class_methods do
def ****
end
end
ActiveSupport::Concern provides syntactic sugar for common Ruby patterns for module mixins.
When you are using modules as mixins you can't just use self to declare class methods like you would from a class:
module Foo
def self.bar
"Hello World"
end
def instance_method
"Hello World"
end
end
class Baz
include Foo
end
irb(main):010:0> Baz.bar
NoMethodError: undefined method `bar' for Baz:Class
from (irb):10
irb(main):011:0> Foo.bar
=> "Hello World"
irb(main):012:0>
As you can see from the example that actually creates a module method - thats because self is the module. You can use extend instead:
module Foo
def a_class_method
"Hello World"
end
end
class Bar
extend Foo
end
irb(main):049:0> Bar.a_class_method
=> "Hello World"
But that does not let you declare instance methods in the module. Which is not really that useful.
So the solution is to create an inner module which is commonly named ClassMethods and extend the class when the module is included:
module Foo
# this is a method thats called when you include the module in a class.
def self.included(base)
base.extend ClassMethods
end
def an_instance_method
end
# the name ClassMethods is just convention.
module ClassMethods
def a_class_method
"Hello World"
end
end
end
class Bar
include Foo
end
irb(main):071:0> Bar.a_class_method
=> "Hello World"
This boilerplate code is found in almost every Ruby gem/library.
By extending your module with ActiveSupport::Concern you can shorten this to just:
module Foo
extend ActiveSupport::Concern
class_methods do
def a_class_method
"Hello World"
end
end
end
Under the hood ActiveSupport::Concern creates a ClassMethods module and evaluates the block in the context of the module. Dig into the source if you curious about how it actually does this.
It's just for convenience. module ClassMethods is pure Ruby, but class_methods is defined in ActiveSupport::Concern for convenience. If you look at a source code you'll find that class_methods does exactly the same thing
# activesupport/lib/concern.rb
def class_methods(&class_methods_module_definition)
mod = const_defined?(:ClassMethods, false) ?
const_get(:ClassMethods) :
const_set(:ClassMethods, Module.new)
mod.module_eval(&class_methods_module_definition)
end
class_methods is used to add class methods to the model used by the concern.
A typical module looks like this:
module M
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
...
end
end
By using ActiveSupport::Concern the above module could instead be written as:
require 'active_support/concern'
module M
extend ActiveSupport::Concern
class_methods do
...
end
end
As Oleg Antonyan pointed out, from the source code, we know that it's going to use ClassMethods module under the hood.
Reference: http://api.rubyonrails.org/classes/ActiveSupport/Concern.html

Why has the InstanceMethods module been deprecated?

I love ActiveSupport::Concern.
It makes it easy to add functionality to your classes, with a nice syntax.
Anyways, in Rails 3.2, the InstanceMethods module has been deprecated. If I understood correctly, we should just define our methods in the included block (actually it's just in the body of the module):
# edit: don't do this! The method definition should just be in the body of the module
included do
def my_method; end
end
I was just wondering if anyone knows why they decided to do that?
Let's look at the example you linked first.
module TagLib
extend ActiveSupport::Concern
module ClassMethods
def find_by_tags()
# ...
end
end
module InstanceMethods
def tags()
# ...
end
end
end
When you include TagLib into your class AS Concern automatically extends the class with ClassMethods module and includes InstanceMethods module.
class Foo
include TagLib
# is roughly the same as
include TagLib::InstanceMethods
extend TagLib::ClassMethods
end
But as you may noticed we are already including TagLib module itself so the methods defined within it are already available as instance methods on the class. Why would you want to have a separate InstanceMethods module then?
module TagLib
extend ActiveSupport::Concern
module ClassMethods
def find_by_tags()
# ...
end
end
def tags()
# ...
end
end
class Foo
include TagLib
# does only `extend TagLib::ClassMethods` for you
end

Make ClassMethods also available as module function with ActiveSupport::Concern

Given the following code:
module Foo
extend ActiveSupport::Concern
module ClassMethods
def foo
puts 'foo'
end
end
end
class Bar
include Foo
end
What I'd like to do is call Foo.foo instead of Bar.foo. Sometimes it feels more natural to call a class method on the original module, especially when the functionality has nothing to do with the included class and is better described along with the original module name.
This seems like a code smell. Having said that, you can just have the Foo module extend itself with the class methods:
module Foo
extend ActiveSupport::Concern
module ClassMethods
def foo
puts 'foo'
end
end
extend ClassMethods
end
class Bar
include Foo
end
Bar.foo
Foo.foo

Resources