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
Related
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
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
I want to dynamically generate a class method in a Mixin, based on the class name that include this Mixin.
Here is my current code:
module MyModule
extend ActiveSupport::Concern
# def some_methods
# ...
# end
module ClassMethods
# Here is where I'm stuck...
define_method "#{self.name.downcase}_status" do
# do something...
end
end
end
class MyClass < ActiveRecord::Base
include MyModule
end
# What I'm trying to achieve:
MyClass.myclass_status
But this give me the following method name:
MyClass.mymodule::classmethods_status
Getting the base class name inside the method definition works (self, self.name...) but I can't make it works for the method name...
So far, I've tried
define_method "#{self}"
define_method "#{self.name"
define_method "#{self.class}"
define_method "#{self.class.name}"
define_method "#{self.model_name}"
define_method "#{self.parent.name}"
But none of this seems to do the trick :/
Is there any way I can retrieve the base class name (not sure what to call the class that include my module). I've been struggling with this problem for hours now and I can't seem to figure out a clean solution :(
Thanks!
I found a clean solution: using define_singleton_method (available in ruby v1.9.3)
module MyModule
extend ActiveSupport::Concern
included do
define_singleton_method "#{self.name}_status" do
# do stuff
end
end
# def some_methods
# ...
# end
module ClassMethods
# Not needed anymore!
end
end
You can't do it like that - at this point it is not yet known which class (or classes) are including the module.
If you define a self.included method it will be called each time the module is included and the thing doing the including will be passed as an argument. Alternatively since you are using AS::Concern you can do
included do
#code here is executed in the context of the including class
end
You can do something like this:
module MyModule
def self.included(base)
(class << base; self; end).send(:define_method, "#{base.name.downcase}_status") do
puts "Hey!"
end
base.extend(ClassMethods)
end
module ClassMethods
def other_method
puts "Hi!"
end
end
end
class MyClass
include MyModule
end
MyClass.myclass_status
MyClass.other_method
Works for extend:
module MyModule
def self.extended who
define_method "#{who.name.downcase}_status" do
p "Inside"
end
end
end
class MyClass
extend MyModule
end
MyClass.myclass_status
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
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