解读Rails - 属性方法

此文翻译自Reading Rails - Attribute Methods,限于本人水平,翻译不当之处,敬请指教!html

在咱们上一篇的探讨中,咱们已经看到了Rails在跟踪属性变动中使用到的属性方法(attribute methods)。有三种类型的属性方法:前缀式(prefix)、后缀式(suffix)以及固定词缀式( affix)。为了表述简洁,咱们将只关注相似attribute_method_suffix这样的后缀式属性方法,而且特别关注它是如何帮助咱们实现相似name这样的模型属性以及对应生成的相似name_changed?这样的方法的。
git

若是须要跟着个人步骤走,请使用qwandry打开每个相关的代码库,或者直接从github查看源码便可。github

声明(Declarations)

属性方法是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编辑器

如今咱们来看一下AttributeMethodMatcherfetch

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又是如何使用它们的呢?

经过Method Missing调用(Invocation With Method Missing)

当咱们调用了一个未定义的方法时,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?方法,或者是你定义的任意一个特殊的属性方法。

元编程(Metaprogramming)

有不少的方式能够对一个方法的调用进行分发(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方法的实现,这种方法的开销要小得多。

总结(Recap)

咱们发现了调用attribute_method_suffix方法会保存一个配置好的对象,这个对象用于Rails中两种元编程方法中的一种。不考虑是否使用了method_missing,或者经过module_eval定义了新的方法,方法的调用最后总会被传递到诸如attribute_changed?(attr)这样的方法上。

走过此次比较宽泛的旅途,咱们也收获了一些有用的技巧:

  • 你必须使用Hash#fetch从options中读取参数,特别是对于boolean类型参数来讲。
  • 诸如"%s_changed"这样的格式化字符串,能够被用于简单的模板。
  • 可使用Regexp.escapeescape正则表达式。
  • 当你试图调用一个未定义的方法时,Ruby会调用method_missing方法。
  • HEREDOCs能够用在方法参数中,也能够用来定义多行的字符串。
  • __FILE__以及__LINE__指向当前的文件以及行号。
  • 你可使用module_eval动态生成代码。

坚持浏览Rails的源代码吧,你总会发现你本来不知道的宝藏!

喜欢这篇文章?

阅读另外8篇《解读Rails》中的文章。

相关文章
相关标签/搜索