关于Mixin和ActiveSupport::Concern

#ruby #rails #activesupport

##ruby对象模型

实际上ActiveSupport::Concern是对mixin的一些通用的模式进行的一个封装。而mixin就像是一个“虚拟的类”,用来注入到class或者module的祖先链中去。

module MyMod
end

class Base
end

class Child < Base
  include MyMod
end

# irb> Child.ancestors
#  => [Child, MyMod, Base, Object, Kernel, BasicObject]

上面的情况与下面这段代码有着相同的祖先链结构

class Base
end

class MyMod < Base
end

class Child < MyMod
end

# irb> Child.ancestors
#  => [Child, MyMod, Base, Object, Kernel, BasicObject]

模型也可以用来作为扩展一个对象

my_obj = Object.new
my_obj.extend MyMod

# irb> my_obj.singleton_class.ancestors
#  => [MyMod, Object, Kernel, BasicObject]

在Ruby中,每个对象都有一个eigenclass,Object#extend的行为与Module#include的行为是相同的,只是前者是作用在对象的eigenclass上面。所以下面的代码和上面的代码效果一致。

my_obj = Object.new
my_obj.singleton_class.class_eval do
  include MyMod
end

# irb> my_obj.singleton_class.ancestors
#  => [MyMod, Object, Kernel, BasicObject]

这就是ruby中静态方法和类方法实现的方式。实习上,ruby中并没有像java中那样的静态方法和类方法。只有在类的eigenclass中的普通方法。下面两种情况的的行为是相同的。

class MyClass
  extend MyMod
end

# irb> MyClass.singleton_class.ancestors
#  => [MyMod, Class, Module, Object, Kernel, BasicObject]

class MyClass
  class << self
    include MyMod
  end
end

# irb> MyClass.singleton_class.ancestors
#  => [MyMod, Class, Module, Object, Kernel, BasicObject]

实际上在ruby中类也是对象。

Mixin

ruby提供了一些钩子方法,当module被混入到类和模块中的时候,这些钩子方法就会被调用,比如

module MyMod
  def self.included(base)
    base.send(:include, InstanceMethods)
    base.extend ClassMethods
    base.class_eval do
      a_class_method
    end
  end

  module InstanceMethods
    def an_instance_method
    end
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end

# irb> class MyClass
# irb>   include MyMod
# irb> end
# a_class_method called
end

# irb> MyClass.ancestors
#  => [MyClass, MyMod::InstanceMethods, MyMod, Object, Kernel, BasicObject]
# irb> MyClass.singleton_class.ancestors
#  => [MyMod::ClassMethods, Class, Module, Object, Kernel, BasicObject]

一个单独的模块添加了实例方法、“类方法”以及直接在base类中执行它(此例中名为a_class_method)。

##ActiveSupport::Concern

下面是用ActiveSupport::concern重写的代码:

module MyMod
  extend ActiveSupport::Concern

  included do
    a_class_method
  end

  def an_instance_method
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end

其中嵌套的InstanceMethods被移除了,an_instance_method()被直接定义在module中。因为在MyClass的祖先链中([MyClass, MyMod::InstanceMethods, MyMod, Object, Kernel]),并不需要`MyMod::InstanceMethods方法,因为MyMod中的方法已经存在在祖先链中了。

同时ActiveSupport::Concern也去掉了一些之前样板模式中的一些代码:不再需要定义 included钩子了,也不再需要自己去 extend base类中的 ClassMethods了,也不需要手动调用 class_eval了。

惰性求值(lazy evaluation)

ActiveSupport::Concern还提供了一个叫做懒惰求值的特性(lazy evaluation)。先来看下普通的mixin

module MyModA
end

module MyModB
  include MyModA
end

class MyClass
  include MyModB
end

# irb> MyClass.ancestors
#  => [MyClass, MyModB, MyModA, Object, Kernel, BasicObject]

如果想要在想要在included的时候在base类中做一些特别的事情,如

module MyModA
  def self.included(base)
    base.class_eval do
      has_many :squirrels
    end
  end
end

这时候,MyModA会被MyModBinclud,included()中的代码将会运行,如果has_many在MyModB中还未定义的话,那么就会报错。

irb :050 > module MyModB
irb :051?>   include MyModA
irb :052?> end
NoMethodError: undefined method `has_many' for MyModB:Module
 from (irb):46:in `included'
 from (irb):45:in `class_eval'
 from (irb):45:in `included'
 from (irb):51:in `include'

而在ActiveSupport::Concern中,将会延迟所有included钩子,只到模块被一个非ActiveSupport::Concern包含。

 module MyModA
  extend ActiveSupport::Concern

  included do
    has_many :squirrels
  end
end

module MyModB
  extend ActiveSupport::Concern
  include MyModA
end

class MyClass
  def self.has_many(*args)
    puts "has_many(#{args.inspect}) called"
  end

  include MyModB
end
# irb>
# has_many([:squirrels]) called


# irb> MyClass.ancestors
#  => [MyClass, MyModB, MyModA, Object, Kernel, BasicObject]
# irb> MyClass.singleton_class.ancestors
#  => [Class, Module, Object, Kernel, BasicObject]

###模块依赖

最后ActiveSupport::concern还能够很好的解决模块依赖关系,如果给两个模块Foo和Bar,其中Bar依赖与Foo,一般都会这么写

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

但是,host为啥要关心Bar的依赖关系呀?照理说应该这样才符合懒人的习惯

module Bar
  include Foo
  def self.included(base)
    base.method_injected_by_foo
  end
end

class Host
  include Bar
end

不幸的是,这样是会报错的。当Foo被include的时候,他的base是Bar模块而不是Host类。而如果使用ActiveSupport::Concern的话,模块的依赖关系就能够很好的解决啦

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 # works, Bar takes care now of its dependencies
end

##参考资料

comments powered by Disqus