分享一下自己阅读 ActiveSupport::Concern 源码的过程,希望和大家一起学习,错误之处,还请指出
在查看 ActiveSupport::Concern 源码之前,我们先理解几个概念
首先从名字可以得到的信息是,instance_eval
的调用者receiver
必须是一个实例instance
,而在instance_eval
block 的内部,self 即为 receiver 实例本身。
# 例子一
obj_instance.instance_eval do
self # => obj_instance
# current class => obj_instance's singleton class
end
根据这个定义,如果在一个实例上调用了instance_eval
,就可以在其中定义该实例的单态函数singleton_method
# 例子二
class A
end
a = A.new
a.instance_eval do
puts self # => a
# current class => a's singleton class
def method1
puts "this is a singleton method of instance a"
end
end
a.method1
#=> this is a singleton method of instance a
b = A.new
b.method1
#=> NoMethodError: undefined method `method1' for #<A:0x007fbc2ced9550>
from (pry):13:in `<main>'
如我们所知,因为类本身也是 Class 类的一个实例,instance_eval 也可以用在类上,这个时候就可以在其中定义该类的 singleton_method,即为该类的类方法。
# 例子三
class A
end
A.instance_eval do
puts self # => A
# current class => A's singleton class
def method1
puts 'this is a singleton method of class A'
end
end
A.method1
# this is a singleton method of class A
#=> nil
#=> NoMethodError: undefined method `method1' for #<A:0x007fbc3009e180>
from (pry):11:in `<main>'
再来看class_eval
,首先从名字可以得到的信息是,class_eval 的调用者 receiver 必须是一个类,而在class_eval
block
的内部,self
即为 receiver 类本身。
# 例子四
class A
end
A.class_eval do
puts self
end
# => A
根据这个定义,如果在一个类上调用了 class_eval,就可以在其中定义该类的实例方法 (instance_method),例如
# 例子五
class A
end
a = A.new
a.method1
#=> NoMethodError: undefined method `method1' for <A:0x007fbc29a826f8> from (pry):21:in `<main>'
A.class_eval do
def method1
puts 'this is a instance method of class A'
end
end
a.method1
#=> this is a instance method of class A
综合上面例子,我们可得出
1. instance_eval
必须由 instance 来调用,可以用来定义单例函数(singleton_methods
)
2. class_eval
必须是由 class 来调用,可以用来定义类的实例方法 (instance_methods
)
废话不多说,先看代码:
# 例子六
module Foo
def foo
puts 'foo method with include...'
end
end
class Bar
include Foo
end
Bar.new.foo
# foo method with include...
# => nil
Bar.foo
# NoMethodError: undefined method `foo' for Bar:Class
# from (pry):75:in `<main>'
class Baz
extend Foo
end
Baz.foo
# foo method with include...
# => nil
Baz.new.foo
# NoMethodError: undefined method `foo' for #<Baz:0x007f8061dec068>
# from (pry):80:in `<main>'
由例子我们可以看出,include
会把module
的方法变成实例方法,extend
会把方法变成类方法。
但是,大多时候我们也可以用include
来实现类方法和实例方法,请看以下例子:
# 例子七
module Foo
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def bar
puts 'class method'
end
end
def foo
puts 'instance method'
end
end
class Baz
include Foo
end
Baz.bar
# class method
# => nil
Baz.new.foo
# instance method
# => nil
Baz.foo
# NoMethodError: undefined method `foo' for Baz:Class
Baz.new.bar
# NoMethodError: undefined method `bar' for #<Baz:0x007fbc30274ab8>
从例子我们可以看出,include
有一个叫included
的钩子,正是通过这个钩子,我们可以用include
实现添加类方法和实例方法
我们来看看included
这个钩子到底做了什么?
module A
def self.included(mod)
puts "#{self} included in #{mod}"
end
end
module Enumerable
include A
end
# A included in Enumerable
# => Enumerable
从代码我们的输出我们可以知道,included
类方法作用域self
为Module A
,并传入include A
的receiver Enumerable
。然后我们再看看例子七,结合extend
与include
的理解,就能明白其中的原理所在。
再来看看 ActiveSupport::Concern 的实现:
ActiveSupport::Concern
之前,我们可以这样进行模块分离module M
def self.included(base)
base.extend ClassMethods
base.class_eval do
scope :disabled, -> { where(disabled: true) }
end
include InstanceMethods
end
module ClassMethods
def say_hello
puts "say hello"
end
end
module InstanceMethods
def say_no
puts "say no"
end
end
end
从代码可知,通过extend
和class_eval
为base
定义了ClassMethods
里面的类方法,通过include
为base
定义了InstanceMethods
里面的实力方法。
当我们引入ActiveSupport::Concern
之后,以上例子我们可以这样写:
require 'active_support/concern'
module M
extend ActiveSupport::Concern
included do
scope :disabled, -> { where(disabled: true) }
include InstanceMethods
end
module ClassMethods
def say_hello
puts "say hello"
end
end
module InstanceMethods
def say_no
puts "say no"
end
end
end
最后,我们再来看看ActiveSupport::Concern,是不是就很好理解了?
module Concern
class MultipleIncludedBlocks < StandardError #:nodoc:
def initialize
super "Cannot define multiple 'included' blocks for a Concern"
end
end
def self.extended(base) #:nodoc:
base.instance_variable_set(:@_dependencies, [])
end
def append_features(base)
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
return false
else
return false if base < self
@_dependencies.each { |dep| base.include(dep) }
super
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end
def included(base = nil, &block)
if base.nil?
raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
@_included_block = block
else
super
end
end
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
end
Rails4.1 之后,Module 还加入了Concerning方法
我们先来看看细化 concern 的几种方法
class Todo
# Other todo implementation
# ...
## Event tracking
has_many :events
before_create :track_creation
after_destroy :track_deletion
private
def track_creation
# ...
end
end
ActiveSupport::Concern
class Todo
# Other todo implementation
# ...
module EventTracking
extend ActiveSupport::Concern
included do
has_many :events
before_create :track_creation
after_destroy :track_deletion
end
private
def track_creation
# ...
end
end
include EventTracking
end
concerning
class Todo < ActiveRecord::Base
# Other todo implementation
# ...
concerning :EventTracking do
included do
has_many :events
before_create :track_creation
after_destroy :track_deletion
end
private
def track_creation
# ...
end
end
end
Todo.ancestors
# [Todo,Todo::EventTracking,...]
总得来说,concerning
主要用于切分比较小的 model
另外,还有还提供了类似的concern
等方法
concern :EventTracking do
end
is equivalent to
module EventTracking
extend ActiveSupport::Concern
end
最后贴上自用 sublime snippet,但是我是 Vim 党。
Reference: http://www.railstips.org/blog/archives/2009/05/15/include-vs-extend-in-ruby/ https://github.com/rails/rails/blob/master/activesupport/lib/active_support/concern.rb https://github.com/rails/rails/blob/master/activesupport/lib/active_support/concern.rb
补充:
def append_features(base)
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
return false
else
return false if base < self
@_dependencies.each { |dep| base.include(dep) }
super
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end
append_features
也是 module 的一个 callback,会在 include 之后,为当前 class 添加 module 的变量,常量,方法等。append_features 会先与 included 被调用,详见:append_features
上面的代码中,如@neverlandxy_naix所说的一样,正是通过递归的方法处理多重嵌套
首先看到一个if
判断,这里判断当前类 (base) 是否定义了@_dependencies
,如果被定义,则把当前 module 加入@_dependencies
。怎么说呢?我们再来看看
def self.extended(base) #:nodoc:
base.instance_variable_set(:@_dependencies, [])
end
extended
类似于included
,具体用法见extended
看完extended
的用法,我们知道,如果当前类extend
了ActiveSupport::Concern
,则@_dependencies
会被定义。
第二个 if 判断,判断当前类是否继承于当前模块,如果是,则不需要做其他操作,如果不是,则说明当前类既不是当前模块的子类,也没有extend
ActiveSupport::Concern
。也就是我们最终要include
当前模块的类,此时,当前类include
当前模块所有依赖@_dependencies
,并定义ClassMethods
和included
block 里面的方法。