ActiveModel::AttributeMethods的用例与机制
跟踪一下define_attribute_method注释中的例子
[1] pry(main)> binding.trace_tree(htmp: 'rails/attribute_methods') do
[1] pry(main)* class Person
[1] pry(main)* include ActiveModel::AttributeMethods
[1] pry(main)*
[1] pry(main)* attr_accessor :name
[1] pry(main)* attribute_method_suffix '_short?'
[1] pry(main)*
[1] pry(main)* # Call to define_attribute_method must appear after the
[1] pry(main)* # attribute_method_prefix, attribute_method_suffix or
[1] pry(main)* # attribute_method_affix declarations.
[1] pry(main)* define_attribute_method :name
[1] pry(main)*
[1] pry(main)* private
[1] pry(main)*
[1] pry(main)* def attribute_short?(attr)
[1] pry(main)* send(attr).length < 5
[1] pry(main)* end
[1] pry(main)* end
[1] pry(main)* end
=> :attribute_short?
完整调用栈如下
首先,include了ActiveModel::AttributeMethods的类都会带有attribute_aliases和attribute_method_matchers类变量
included do
class_attribute :attribute_aliases, :attribute_method_matchers, instance_writer: false
self.attribute_aliases = {}
self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new]
end
检查这两个类变量,会发现在刚才整套Person类定义下来,attribute_method_matchers除了included时的ClassMethods::AttributeMethodMatcher.new,还多了一个attribute_short?的ActiveModel::AttributeMethods::ClassMethods::AttributeMethodMatcher
[3] pry(main)> person.attribute_method_matchers
=> [#<activemodel::attributemethods::classmethods::attributemethodmatcher:0x007f7b1a45d520 @method_missing_target="attribute" ,="" @method_name="%s" ,="" @prefix="" ,="" @regex="/^(?:)(.*)(?:)$/," @suffix="">,
#<activemodel::attributemethods::classmethods::attributemethodmatcher:0x007f7b1a24fc88 @method_missing_target="attribute_short?" ,="" @method_name="%s_short?" ,="" @prefix="" ,="" @regex="/^(?:)(.*)(?:_short\?)$/," @suffix="_short?">]
[4] pry(main)> person.attribute_aliases
=> {}</activemodel::attributemethods::classmethods::attributemethodmatcher:0x007f7b1a24fc88></activemodel::attributemethods::classmethods::attributemethodmatcher:0x007f7b1a45d520>
此外,因为ActiveModel::AttributeMethods是有concern的,所以Person会获得以下类方法
[9] pry(main)> ActiveModel::AttributeMethods::ClassMethods.instance_methods
=> [:alias_attribute,
:define_attribute_methods,
:generated_attribute_methods,
:undefine_attribute_methods,
:attribute_method_prefix,
:attribute_method_suffix,
:attribute_method_affix,
:attribute_alias?,
:attribute_alias,
:define_attribute_method]
接着来看Person用到的attribute_method_suffix和define_attribute_method。首先是attribute_method_suffix,它所做的就是两件事:新建一个AttributeMethodMatcher,然后undefine_attribute_methods
def attribute_method_suffix(*suffixes)
self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new suffix: suffix }
undefine_attribute_methods
end
AttributeMethodMatcher的作用,从其内部的一些命名来看,似乎是用于method_missing时,检查是否能调用有所call的带有prefix/suffix的方法
class AttributeMethodMatcher
attr_reader :prefix, :suffix, :method_missing_target
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
AttributeMethodMatch.new(method_missing_target, $1, method_name)
end
end
def method_name(attr_name)
@method_name % attr_name
end
def plain?
prefix.empty? && suffix.empty?
end
end
而undefine_attribute_methods则是……比较复杂,目前只看出它使用到一个匿名module,这个匿名module会被include到当前类中,估计用于定义方法并归纳它们,但instance_methods.each { |m| undef_method(m) }和attribute_method_matchers_cache又是为什么呢?暂时先不管
def undefine_attribute_methods
generated_attribute_methods.module_eval do
instance_methods.each { |m| undef_method(m) }
end
attribute_method_matchers_cache.clear
end
def generated_attribute_methods
@generated_attribute_methods ||= Module.new {
extend Mutex_m
}.tap { |mod| include mod }
end
private
# The methods +method_missing+ and +respond_to?+ of this module are
# invoked often in a typical rails, both of which invoke the method
# +matched_attribute_method+. The latter method iterates through an
# array doing regular expression matches, which results in a lot of
# object creations. Most of the time it returns a +nil+ match. As the
# match result is always the same given a +method_name+, this cache is
# used to alleviate the GC, which ultimately also speeds up the app
# significantly (in our case our test suite finishes 10% faster with
# this cache).
def attribute_method_matchers_cache
@attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
end
于是,便轮到示例所说的只能(还是必须?)在attribute_method_prefix、attribute_method_suffix、attribute_method_affix之后使用的define_attribute_method,其相关代码如下
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
private
def define_proxy_call(include_private, mod, name, send, *extra)
defn = if NAME_COMPILABLE_REGEXP.match?(name)
"def #{name}(*args)"
else
"define_method(:'#{name}') do |*args|"
end
extra = (extra.map!(&:inspect) << "*args").join(", ".freeze)
target = if CALL_COMPILABLE_REGEXP.match?(send)
"#{"self." unless include_private}#{send}(#{extra})"
else
"send(:'#{send}', #{extra})"
end
mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
#{defn}
#{target}
end
RUBY
end
简单来说,就是遍历已定义的prefix/suffix/affix(attribute_method_matchers),拼接出最终想要的方法名,即prefix + attr_name + suffix,然后define到匿名module(generated_attribute_methods)中,对于本例,就是def一个name_short?方法。
而unless instance_method_already_implemented?的作用,估计是为了避免调用define_attribute_method时给了重复的属性名,导致重复定义prefix + attr_name + suffix方法
而prefix + attr_name + suffix的方法体,即target部分,可以推导出,是send matcher.method_missing_target, attr_name.to_s,其中method_missing_target是AttributeMethodMatcher 约定的"#{@prefix}attribute#{@suffix}",attr_name.to_s则是属性名,对于本例,大概就是send attribute_short?(:name)。因此,如示例所述,如果你指定了suffix方法_short?,则你需要定义一个attribute_short?(attr)方法,以便在匿名module调用时,能查找到子类(当前类)的这个方法。所以,name_short?的调用栈非常简单
[29] pry(main)> person.name = 'ken'
=> "ken"
[30] pry(main)> binding.trace_tree(htmp: 'rails/call_attribute_method'){person.name_short?}
=> true
如下
至此,ActiveModel::AttributeMethods的机制基本已明了。若想看复杂一点的用例,源码开头也是有给的。或者,也可看看dirty.rb
class Person
include ActiveModel::AttributeMethods
attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
attribute_method_suffix '_contrived?'
attribute_method_prefix 'clear_'
define_attribute_methods :name
attr_accessor :name
def attributes
{ 'name' => @name }
end
private
def attribute_contrived?(attr)
true
end
def clear_attribute(attr)
send("#{attr}=", nil)
end
def reset_attribute_to_default!(attr)
send("#{attr}=", 'Default Name')
end
end
至于undefine_attribute_methods,它规定了你所有attribute_method_xxx必须定在所有define_attribute_method(s)之前,否则,后来的一个attribute_method_xxx中的undefine_attribute_methods就会抹掉之前在匿名module中动态定义的方法。可以认为它是为了规范代码结构,令你的pre/suf方法定义凑在一起,而非分散到文件的头部尾部。当然,也可以认为它是一个坑
[35] pry(main)> class Person
[35] pry(main)* attribute_method_suffix '_long?'
[35] pry(main)* end
=> #
[36] pry(main)> person.name_short?
NoMethodError: undefined method `name_short?' for #<person:0x007f7b1a15e9f0 @name="ken">
from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activemodel-5.1.2/lib/active_model/attribute_methods.rb:432:in `method_missing'</person:0x007f7b1a15e9f0>
此外,也因为这种undef机制,以及匿名module被归为当前类的实例变量,而每次def/undef都从当前类的取出该实例变量来作用域该匿名module,所以,如果想为不同的属性添加不同的pre/suf,则需要将不同的“attribute_method_xxx和define_attribute_method(s)”写在不同的module中,例如dirty.rb就单独开来一个module。这同样也达到了归类pre/suf的目的
而attribute_method_matchers_cache,暂不深究……