此文翻译自Reading Rails - Attribute Methods,限于本人水平,翻译不当之处,敬请指教!html
在咱们上一篇的探讨中,咱们已经看到了Rails在跟踪属性变动中使用到的属性方法(attribute methods)。有三种类型的属性方法:前缀式(prefix)、后缀式(suffix)以及固定词缀式( affix)。为了表述简洁,咱们将只关注相似attribute_method_suffix
这样的后缀式属性方法,而且特别关注它是如何帮助咱们实现相似name
这样的模型属性以及对应生成的相似name_changed?
这样的方法的。
git
若是须要跟着个人步骤走,请使用qwandry打开每个相关的代码库,或者直接从github查看源码便可。github
属性方法是Rails中众多使用了元编程技术的案例之一。在元编程中,咱们编写能够编写代码的代码。举例来讲,attribute_method_suffix
后缀式方法是一个为每一个属性都定义了一个helper方法的方法。在以前的讨论中,ActiveModel使用这种方式为您的每个属性都定义了一个_changed?
方法(提示: 命令行中键入qw activemodel
查看代码):正则表达式
module Dirty extend ActiveSupport::Concern include ActiveModel::AttributeMethods included do attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' #...
让咱们打开ActiveModel库中的attribute_methods.rb
文件,而且看一下到底发生了什么事情。编程
def attribute_method_suffix(*suffixes) self.attribute_method_matchers += suffixes.map! do |suffix| AttributeMethodMatcher.new suffix: suffix end #... end
当你调用attribute_method_suffix
方法的时候,每个后缀都经过map!
方法转换为一个AttributeMethodMatcher
对象。这些对象会被存储在attribute_method_matchers
中。若是你从新看一下这个module的顶部,你会发现attribute_method_matchers
是在每个包含此module的类中使用class_attribute
定义的方法:ruby
module AttributeMethods extend ActiveSupport::Concern included do class_attribute :attribute_aliases, :attribute_method_matchers, instance_writer: false #...
class_attribute
方法帮助你在类上定义属性。你能够这样在你本身的代码中这样使用:服务器
class Person class_attribute :database #... end class Employee < Person end Person.database = Sql.new(:host=>'localhost') Employee.database #=> <Sql:host='localhost'>
Ruby中并无class_attribute
的内置实现,它是在ActiveSupport(提示:命令行中键入qw activesupport
查看代码)中定义的方法。若是你对此比较好奇,能够简单看下attribute.rb
编辑器
如今咱们来看一下AttributeMethodMatcher
。fetch
class AttributeMethodMatcher #:nodoc: attr_reader :prefix, :suffix, :method_missing_target 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
代码中的prefix
以及suffix
是经过Hash#fetch
方法提取出来的。这会返回一个对应键的值,或者是一个默认值。若是调用方法的时候没有提供默认值,Hash#fetch
方法将会抛出一个异常,提示指定的键不存在。对于options的处理来讲是一种不错的模式,特别是对于boolean型数据来讲:ui
options = {:name => "Mortimer", :imaginary => false} # Don't do this: options[:imaginary] || true #=> true # Do this: options.fetch(:imaginary, true) #=> false
对于咱们的attribute_method_suffix
其中的'_changed'
示例来讲,AttributeMethodMatcher
将会有以下的实例变量:
@prefix #=> "" @suffix #=> "_changed?" @regex #=> /^(?:)(.*)(?:_changed\?)$/ @method_missing_target #=> "attribute_changed?" @method_name #=> "%s_changed?"
你必定想知道%s_changed
中的%s
是用来干什么的吧?这是一个格式化字符串(format string)。你可使用sprintf
方法对它插入值,或者使用缩写(shortcut)%
:
sprintf("%s_changed?", "name") #=> "named_changed?" "%s_changed?" % "age" #=> "age_changed?"
第二个比较有趣的地方就是正则表达式建立的方式。请留意建立@regex
变量时Regexp.escape
的用法。若是后缀没有被escape,则正则表达式中带有特殊含义的符号将会被错误解释(misinterpreted):
# Don't do this! regex = /^(?:#{@prefix})(.*)(?:#{@suffix})$/ #=> /^(?:)(.*)(?:_changed?)$/ regex.match("name_changed?") #=> nil regex.match("name_change") #=> #<MatchData "name_change" 1:"name"> # Do this: @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/ regex.match("name_changed?") #=> #<MatchData "name_changed?" 1:"name"> regex.match("name_change") #=> nil
请仔细记住regex
以及method_name
,它们能够用来匹配和生成属性方法,咱们在后面还会继续用到它们。
咱们如今已经搞明白了属性方法是如何声明的,可是实际中,Rails又是如何使用它们的呢?
当咱们调用了一个未定义的方法时,Rails将会在抛出异常以前调用对象的method_missing
方法。让咱们看看Rails是如何利用这个技巧调用属性方法的:
def method_missing(method, *args, &block) if respond_to_without_attributes?(method, true) super else match = match_attribute_method?(method.to_s) match ? attribute_missing(match, *args, &block) : super end end
传给method_missing
方法的第一个参数是一个用symbol类型表示的方法名,好比,咱们的:name_changed?
。*args
是(未定义的)方法被调用时传入的全部参数,&block
是一个可选的代码块。Rails首先经过调用respond_to_without_attributes
方法检查是否有别的方法能够对应此次调用。若是别的方法能够处理此次调用,则经过super
方法转移控制权。若是找不到别的方法能够处理当前的调用,ActiveModel则会经过match_attribute_method?
方法检查当前调用的方法是不是一个属性方法。若是是,它则会接着调用attribute_missing
方法。
match_attribute_method
方法利用了以前声明过的AttributeMethodMatcher
对象:
def match_attribute_method?(method_name) match = self.class.send(:attribute_method_matcher, method_name) match if match && attribute_method?(match.attr_name) end
在这个方法里边发生了两件事。第一,Rails查找到了一个匹配器(matcher),而且检查这是否真的是一个属性。说实话,我本身也是比较迷惑,为何match_attribute_method?
方法调用的是self.class.send(:attribute_method_matcher, method_name)
,而不是self.attribute_method_matcher(method_name)
,可是咱们仍是能够假设它们的效果是同样的。
若是咱们再接着看attribute_method_matcher
,就会发现它的最核心的代码仅仅只是扫描匹配了AttributeMethodMatcher
实例,它所作的事就是对比对象自己的正则表达式与当前的方法名:
def attribute_method_matcher(method_name) #... attribute_method_matchers.detect { |method| method.match(method_name) } #... end
若是Rails找到了匹配当前调用的方法的属性,那么接下来全部参数都会被传递给attribute_missing
方法:
def attribute_missing(match, *args, &block) __send__(match.target, match.attr_name, *args, &block) end
这个方法将匹配到的属性名以及传入的任意参数或者代码块代理给了match.target
。回头看下咱们的实例变量,match.target
将会是attribute_changed?
,并且match.attr_name
则是"name"。__send__
方法将会调用attribute_changed?
方法,或者是你定义的任意一个特殊的属性方法。
有不少的方式能够对一个方法的调用进行分发(dispatch),若是这个方法常常被调用,那么实现一个name_changed?
方法将会更为有效。Rails经过define_attribute_methods
方法作到了对这类属性方法的自动定义:
def define_attribute_methods(*attr_names) attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) } end def define_attribute_method(attr_name) attribute_method_matchers.each do |matcher| method_name = matcher.method_name(attr_name) define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s end end
matcher.method_name
使用了咱们前面见到过的格式化字符串,而且插入了attr_name
。在咱们的例子中,"%s_changed?"
变成了"name_changed?"
。如今咱们咱们准备好了了解在define_proxy_call
中的元编程。下面是这个方法被删掉了一些特殊场景下的代码的版本,你能够在阅读完这篇文章后本身去了解更多的代码。
def define_proxy_call(include_private, mod, name, send, *extra) defn = "def #{name}(*args)" extra = (extra.map!(&:inspect) << "*args").join(", ") target = "#{send}(#{extra})" mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1 #{defn} #{target} end RUBY end
这里为咱们定义了一个新的方法。name
就是正要被定义的方法名,而send
则是处理器(handler),另外的extra
是属性名。mod
参数是一个Rails用generated_attribute_methods
方法生成的特殊的模块(module),它被嵌入(mixin)到咱们的类中。如今让咱们多看一下module_eval
方法。这里有三件有趣的事情发生了。
第一件事就是HEREDOC被用做一个参数传给了一个方法。这是有点难懂的,可是对某些场景倒是很是有用的。举个例子,想象咱们在一个服务器响应(response)中有一个方法要用来嵌入Javascript代码:
include_js(<<-JS, :minify => true) $('#logo').show(); App.refresh(); JS
这将会把字符串"$('#logo').show(); App.refresh();"
做为调用include_js
时传入的第一个参数,而:minify => true
做为第二个参数。在Ruby中须要生成代码时,这是一个很是有用的技巧。值得高兴的是,诸如TextMate这类编辑器都可以识别这个模式,而且正确地高亮显示字符串。即便你并不须要生成代码,HEREDOC对于多行的字符串也是比较有用的。
如今咱们就知道了<<-RUBY
作了些什么事,可是__FILE__
以及__LINE__ + 1
呢?__FILE__
返回了当前文件的(相对)路径,而__LINE__
返回了当前代码的行号。module_eval
接收这些参数,并经过这些参数决定新的代码定义在文件中“看起来”的位置。在对于栈跟踪(stack traces)来讲是特别有用的。
最后,让咱们看一些module_eval
中实际执行的代码。咱们能够把值替换成咱们的name_changed?
:
mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1 def name_changed?(*args) attribute_changed?("name", *args) end RUBY
如今name_changed?
就是一个真实的方法了,比起依赖于method_missing
方法的实现,这种方法的开销要小得多。
咱们发现了调用attribute_method_suffix
方法会保存一个配置好的对象,这个对象用于Rails中两种元编程方法中的一种。不考虑是否使用了method_missing
,或者经过module_eval
定义了新的方法,方法的调用最后总会被传递到诸如attribute_changed?(attr)
这样的方法上。
走过此次比较宽泛的旅途,咱们也收获了一些有用的技巧:
Hash#fetch
从options中读取参数,特别是对于boolean类型参数来讲。"%s_changed"
这样的格式化字符串,能够被用于简单的模板。Regexp.escape
escape正则表达式。method_missing
方法。__FILE__
以及__LINE__
指向当前的文件以及行号。module_eval
动态生成代码。坚持浏览Rails的源代码吧,你总会发现你本来不知道的宝藏!
阅读另外8篇《解读Rails》中的文章。