此文翻譯自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》中的文章。