模块是方法和常量的集合,模块和类一样,其中的可以包括两种方法:实例方法(Instance Method)、模块方法(Module Method)。

当一个类include(Mixin)一个模块时,模块中的实例方法会成为该类的实例方法,expand(Mixin)时,模块中的实例方法会成为该类的类方法。两种情况下模块方法都会被忽略。

同时实现类方法和实例方法的混入看起来是鱼和熊掌的问题,要放弃吗?显然是不可能的,我们都是贪婪的。

一个解决办法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
module ClassMethods
end

module InstanceMethods
end

class Foo
    extend ClassMethods
    include InstanceMethods
end

看起来可以正常工作,但是不那么优雅,粒度太大。

聪明的程序员总是能想到解决办法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
module Mod

    def self.included(base)
        base.extend(ClassMothods)
    end

    module ClassMethods
        # 类方法定义
    end

    #实例方法定义
end

class M
    include Mod
end

嗯,看起来不错,比第一种方法优雅多了。但是用着用着问题就来

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module Foo
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def say
      puts 'say'
    end
  end
end

module Bar
  include Foo

  def self.included(base)
      base.say
  end
end

class Host
  include Bar
end

上面代码会抛出NoMethodError: undefined method 'say' for Host:Class异常

原因在于include Bar的时候Fooincluded(base)被执行,此时baseBar

一个解决办法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
module Foo
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def say
      puts 'say'
    end
  end
end

module Bar
  def self.included(base)
      base.say
  end
end

class Host
  include Foo
  include Bar
end

问题虽然解决的,但是引入了一个新的问题,所有使用Bar的地方都要知道它依赖Foo,需要分出额外的精力来维护这种依赖关系,我们应该把他们的依赖关系隐藏起来。

看来我们还是太挑剔

ActiveSupport::Concern

本文重点来了

为了解决这种依赖关系,rails中增加了ActiveSupport::Concern(源码)这个工具。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
module ActiveSupport
  # 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 <tt>ActiveSupport::Concern</tt> 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
  #
  #     module ClassMethods
  #       ...
  #     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 <tt>base</tt>
  # is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
  # 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 # works, Bar takes care now of its dependencies
  #   end
  module Concern
    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.send(: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?
        @_included_block = block
      else
        super
      end
    end
  end
end

实现代码相当简短,使用也非常简单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
module M
    extend ActiveSupport::Concern
    included do
        self.send(:do_host_something)
    end

   module ClassMethods
      def wo
        # do something
      end
   end

   module InstanceMethods
      def ni
         # do something
      end
   end
end

终极版

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
require 'active_support/concern'
module Foo
  extend ActiveSupport::Concern
  module ClassMethods
    def say
      puts 'say'
    end
  end
end

module Bar
  extend ActiveSupport::Concern
  include Foo
  included do
      self.say
  end
end

class Host
  include Bar
end

洗洗睡觉