解讀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》中的文章。

相關文章
相關標籤/搜索