Rails 理解 ActiveSupport::Concern

justin for Beansmile · 2015年06月27日 · 最后由 StephenZzz 回复于 2019年03月31日 · 10559 次阅读
本帖已被管理员设置为精华贴

分享一下自己阅读 ActiveSupport::Concern 源码的过程,希望和大家一起学习,错误之处,还请指出

在查看 ActiveSupport::Concern 源码之前,我们先理解几个概念

class_eval and instance_eval

instance_eval

首先从名字可以得到的信息是,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,首先从名字可以得到的信息是,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)

include and extend

废话不多说,先看代码:

# 例子六
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类方法作用域selfModule A,并传入include Areceiver Enumerable。然后我们再看看例子七,结合extendinclude的理解,就能明白其中的原理所在。

再来看看 ActiveSupport::Concern 的实现:

  1. 在没有引入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

从代码可知,通过extendclass_evalbase定义了ClassMethods里面的类方法,通过includebase定义了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方法

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


补充:

ActiveSupport::Concern#append_features

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的用法,我们知道,如果当前类extendActiveSupport::Concern,则@_dependencies会被定义。

第二个 if 判断,判断当前类是否继承于当前模块,如果是,则不需要做其他操作,如果不是,则说明当前类既不是当前模块的子类,也没有extend ActiveSupport::Concern。也就是我们最终要include当前模块的类,此时,当前类include当前模块所有依赖@_dependencies,并定义ClassMethodsincludedblock 里面的方法。

沙发,大神威武!

赞,之前有段时间也是在折腾 ActiveSupport::Concern……

@martin91 写得不好,还望马丁大神指点一二 😄

@yaocanwei 来个分享嘞!

关键点都没有解释:

  1. append_features 钩子方法是什么时候调用的?
  2. 为什么要重写 included 钩子方法?
  3. 多重 module 嵌套是怎么通过递归去处理 @_dependencies 的?

@neverlandxy_naix

  • append_features 什么时候被调用可能不是本文最主要的,据我所知,append_features 也是 module 的一个 callback,会在 include 之后,为当前 class 添加 module 的变量,常量,方法等。append_features 会先与included 被调用详见:http://apidock.com/ruby/Module/append_features

  • 重写included方法应该是方便以block的方式添加类方法,请对比:

base.class_eval do
  scope :disabled, -> { where(disabled: true) }
end
included do
  scope :disabled, -> { where(disabled: true) }
end
  • 模块依赖通过加载
base.instance_variable_get(:@_dependencies) << self

@_dependencies.each { |dep| base.include(dep) }

这些讲起来确实还得费点时间,我找个时间补充一下吧。

#6 楼 @justin 😆 直接更新到原帖上去好了,可以让更多的人看见

@justin 关于 append_features 的解释在 这里 可以作为一点参考

Concern 我早就想学了

插播一个信息,以上是我厂内训的总结,招聘职位持续开放中: https://ruby-china.org/topics/22244 欢迎大家加入一起学习进步。

#11 楼 @leondu ^_^我再插播个比较近的招聘链接: https://ruby-china.org/topics/24961

#12 楼 @justin 哈哈,感谢,请大家参考上一条。

module M
  def self.included(base)
    base.extend ClassMethods
    base.class_eval do
      scope :disabled, -> { where(disabled: true) }
    end

    include InstanceMethods
 # include InstanceMethods shoule be base.include InstanceMethods
  end

  module ClassMethods
    def say_hello
      puts "say hello"
    end
  end

  module InstanceMethods
    def say_no
      puts "say no"
    end
  end
end


需要 登录 后方可回复, 如果你还没有账号请 注册新账号