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
Related
require 'active_support/concern'
module M
extend ActiveSupport::Concern
included do
def self.b
puts 'b'
end
end
class_methods do
def a
puts 'a'
end
end
end
class H
include M
end
Though class_methods is the standard way to define class methods in ActiveSupport::Concern, I do find a few examples where people declare class methods in included by using self.
Both works, so are there edgecases if class methods are defined inside included?
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 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
I extended my ActiveRecord class by the notable pattern described here and here.
I can't find a way to safely make my new included class method (extend_with_mod_a and extend_with_mod_b) call their own class method (bar)
require 'active_record'
module ModA
extend ActiveSupport::Concern
module ClassMethods
def extend_with_mod_a
puts "extending #{self} with ModA"
bar
end
def bar
puts "this is method bar of ModA"
end
end
end
module ModB
extend ActiveSupport::Concern
module ClassMethods
def extend_with_mod_b
puts "extending #{self} with ModB"
bar
end
def bar
puts "this is method bar of ModB"
end
end
end
ActiveRecord::Base.send :include, ModA
ActiveRecord::Base.send :include, ModB
class TestModel < ActiveRecord::Base
extend_with_mod_a
extend_with_mod_b
end
Output is
extending with ModA
this is method bar of ModB
extending with ModB
this is method bar of ModB
Of course which bar method gets called depends on ActiveRecord include calls order. I would like to use something like ModA::ClassMethods.bar in extend_with_mod_a method definition
module ModA
extend ActiveSupport::Concern
module ClassMethods
def bar
puts "this is method bar of ModA"
end
# grab UnboundMethod of ModA::ClassMethods::bar
a_bar = instance_method(:bar)
# using define_method to capture a_bar
define_method :extend_with_mod_a do
puts "extending #{self} with ModA"
# invoke ModA::ClassMethods::bar properly bound to the class being extended/included with ModA
a_bar.bind(self).call
end
end
end
Try to :prepend your module.
ActiveRecord::Base.send :prepend, ExtModule
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