模块是方法和常量的集合,模块和类一样,其中的可以包括两种方法:实例方法(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
的时候Foo
中included(base)
被执行,此时base
是Bar
一个解决办法
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
|
洗洗睡觉