icon Ruby on Rails 研究

阅读Rails 2.2 的 ActiveRecord::Validations#add 的源代码

下面的是ActiveRecord 2.2.2 的 lib/active_record 目录中的 validations.rb 的摘录。

def add(attribute, message = nil, options = {})
  message ||= :invalid
  message = generate_message(attribute, message, options) if message.is_a?(Symbol)
  @errors[attribute.to_s] ||= []
  @errors[attribute.to_s] << message
end

这一章,将就这个方法对源代码进行解读。

这个 add 方法是 ActiveRecord::Errors 类的实例方法。

某记录对象值的验证 (validation) 失败的话,就在ActiveRecord::Errors 对象被记录。

这个 add 是记录错误的方法。

到 Rails2.1 之前,通过字符串在add 方法的第2参数中指定错误信息。但是,第 2 参数省略的话, 就记录 "is invalid" 的默认错误信息。

从 Rails 2.2起,在第 2 参数指定符号的话,国际化 (i18n) 便运行配合区域选择错误信息。


以行为单位开始读代码。

  message ||= :invalid

如果第 2 参数 messagefalsenil ,便代入符号 :invalid 。通过此处理,使得第2参数省略的情况下,也可以使运行与 Rails 2.1 以前兼容。

  message = generate_message(attribute, message, options) if message.is_a?(Symbol)

如果 message 的类是 Symbol ,就将 generate_message 方法的返回值代入message 本身。

这种方法稍后在说明,照此名称生成错误信息并作为字符串返回。

  @errors[attribute.to_s] ||= []
  @errors[attribute.to_s] << message

实例变量 @errors ,是根据每个字段(属性)来保持错误信息的数组(关联数组)。

这里也使用 ||= 进行初始化。

像这样的,对象在最初必要的时刻进行初期化叫做延迟初期化 (lazy initialization)。


接下来看generate_message 方法的源代码。

def generate_message(attribute, message = :invalid, options = {})

  message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)

  defaults = @base.class.self_and_descendents_from_active_record.map do |klass| 
    [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", 
      :"models.#{klass.name.underscore}.#{message}" ]
  end
  
  defaults << options.delete(:default)
  defaults = defaults.compact.flatten << :"messages.#{message}"

  key = defaults.shift
  value = @base.respond_to?(attribute) ? @base.send(attribute) : nil

  options = { :default => defaults,
    :model => @base.class.human_name,
    :attribute => @base.class.human_attribute_name(attribute.to_s),
    :value => value,
    :scope => [:activerecord, :errors]
  }.merge(options)

  I18n.translate(key, options)
end

真是富有教育意义的有趣的代码啊。

  message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)

options[:default] 为符号的话,将 messageoptions[:default] 的值替换。

在很多传统的语言里,替换变量 a 与 b 的值的时候,有必要像下面这样写。

temp = a
a = b
b = temp

但是,用 Ruby 的话可以写得像下面这样简洁。

a, b = b, a

这叫做多重代入 (parallel assignment)。


  defaults = @base.class.self_and_descendents_from_active_record.map do |klass| 
    [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", 
      :"models.#{klass.name.underscore}.#{message}" ]
  end

首先,@base.class.self_and_descendents_from_active_record 返回作为对象的记录对象的模型类本身和原始类中 ActiveRecord::Base 的分支类的数组。

假定作为对象的记录对象是 Article 模型的实例,Article 模型继承 Document 模型,此外 Document 模型继承 ActiveRecord::Base

这样一来,@base.class.self_and_descendents_from_active_record 便返回名称为 [Article, Document] 的数组。

Ruby 中,类本身也是对象,所以可以作为数组的元素处理。

接下来是 map 方法。collect 方法的别名。

在包含 Enumerable 模块的类(数组等)中,具有这个非常方便的方法。

请看下面的例子。

ary = [1, 2, 3, 4, 5]
ary = ary.map do |n|
  n * n
end

最后变量 ary 中存储了怎样的值呢。

答案是 [1, 4, 9, 16, 25]

从例可知,map 方法将数组元素一个一个传递到块,并收集 (collect) 从块返回的值,生成新的数组并返回。

理解了这个事情之后,返回开始的源代码。

  defaults = @base.class.self_and_descendents_from_active_record.map do |klass| 
    [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", 
      :"models.#{klass.name.underscore}.#{message}" ]
  end

map 方法在变量 klass 中将 ArticleDocument 等模型类一个一个存储,评价块。

块内部生成有两个符号构成的数组。。

这里的写法也让人深感兴趣。

:"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}"

Ruby 中使用 #{ ... } 的写法实现在字符串中嵌入 (interpolate) 式子的值。

而且,在字符串的前面添加冒号 (:) 的话,便可将此字符串变为符号。

attribute 的值为 'title' ,message 的值为 'invalid' 的话,上面的代码便将下面这样的数组代入变量 defaults

[
  [ :"models.article.attributes.title.invalid", :"models.article.invalid" ],
  [ :"models.document.attributes.title.invalid", :"models.document.invalid" ]
]

然后继续。

  defaults << options.delete(:default)

回想一下options 是散列表(关联数组)的事情。

散列表的实例方法 delete 删除被参数指定的键所对应的值并返回其值。

并且在数组 defaults 的末尾追加该值。


  defaults = defaults.compact.flatten << :"messages.#{message}"

defaults 的值是包含符号的数组的数组。通过 compact 方法将数组中包含的 nil 全部除去。然后,通过flatten 方法,将套叠的数组变为 “平坦”的数组。最后追加 :"messages.invalid" ,重新代入defaults 本身。

为理解 flatten 方法的行动,请看下面的代码。

ary = [1, 2, 3, [4, 5, 6], 7]
ary = ary.flatten

最终变量 ary 的值变为像 [1, 2, 3, 4, 5, 6, 7] 这样的非嵌套结构的数组。

  key = defaults.shift

从数组 defaults 中去掉最初的元素,代入变量 key


  value = @base.respond_to?(attribute) ? @base.send(attribute) : nil

实例变量 @base 中,存储了作为此 Errors 对象的对象,记录对象。

respond_to? 方法,调查这个记录对象是否具有某种方法。

现在,变量 attribute 中,应该含有像 :title 一样的符号。

记录对象持有 title 方法的话,@base.respond_to?(attribute) 便被评价为true

这行中使用了三元运算符 (ternary operator)。看下面的例子。

y = x ? 1 : 0

变量 y 的值,x 为“真”时是1,不是时为0。注意在Ruby 中不管是、false 还是 nil 值都被判定为“真”。

原来的代码中变量 value 里存有 @base.send(attribute)nil

send 方法指定方法名称,并调用某个对象的方法。

总之,像 @base.title 这样的方法的返回值被代入变量 value

三元运算符都不是 Ruby 特有的运算符,在受C 影响的很多的程序语言中都予以采用。

因为使用if 的条件执行也能表现,所以也有人不使用三元运算符。

对我来说,像这样的源代码一样条件式很简短的情况下非常地希望使用三元运算符。


  options = { :default => defaults,
    :model => @base.class.human_name,
    :attribute => @base.class.human_attribute_name(attribute.to_s),
    :value => value,
    :scope => [:activerecord, :errors]
  }.merge(options)

这里的要点是散列表的实例方法 merge

请回想一下 options 也是散列表的事情。

方法 merge ,将literally散列表与散列表总结为一个。

请看下面的例子。

a = { :foo => 1, :bar => 2 }
b = { :foo => 3, :baz => 4 }
c = a.merge(b)

在变量 c 中存储的是 { :foo => 3, :bar => 2, :baz => 4 } 的散列表。

这个 merge 方法在赋予选项默认值时经常被使用。作为一种习惯用语记住就好了。


  I18n.translate(key, options)

I18n 模块的 translate 方法,从翻译文件中找到对应第 1 参数所指定的键的字符串并返回。

翻译文件就是放置在 config/locales 目录下的 YAML 文件。

详细信息请参考连载《使基础 Ruby on Rails的 asagao 对应 Rails 2.2》的[http://www.oiax.jp/rails/asagao_2/i18n_1.html 国际化(i18n)的第一歩。

另外,关于 translate 方法的详细使用方法,请参考The Ruby on Rails I18n core api

关于 ActiveRecord::Validations 的实例方法 addgenerate_message的源代码的解说就此结束。


这次的题材里,包含了许多Ruby 独特的表现。

对Ruby 初学者来说可能也有稍微难于理解的地方,但是如果读了这篇文章感觉到愉快的话我将感到非常荣幸。
--
黒田努

(2009/01/17)