解讀Rails - 適配器模式

本文翻譯自Reading Rails - The Adapter Pattern,限於本人水平有限,翻譯不當之處,敬請指教!git

今天咱們暫時先放下具體的代碼片斷,咱們將要對Rails中所實現的一個比較常見的設計模式進行一番探索,這個模式就是適配器模式(Adapter Pattern)。從必定的意義上來講,此次的探索並不全面,可是我但願可以突出一些實際的例子。github

爲了跟隨本文的步驟,請使用qwandry打開相關的代碼庫,或者直接在Github上查看這些代碼。sql

適配器模式

適配器模式能夠用於對不一樣的接口進行包裝以及提供統一的接口,或者是讓某一個對象看起來像是另外一個類型的對象。在靜態類型的編程語言裏,咱們常用它去知足類型系統的特色,可是在相似Ruby這樣的弱類型編程語言裏,咱們並不須要這麼作。儘管如此,它對於咱們來講仍是有不少意義的。數據庫

當使用第三方類或者庫的時候,咱們常常從這個例子開始(start out fine):編程

def find_nearest_restaurant(locator)
  locator.nearest(:restaurant, self.lat, self.lon)
end

咱們假設有一個針對locator的接口,可是若是咱們想要find_nearest_restaurant可以支持另外一個庫呢?這個時候咱們可能就會去嘗試添加新的特殊的場景的處理:json

def find_nearest_restaurant(locator)
  if locator.is_a? GeoFish
    locator.nearest(:restaurant, self.lat, self.lon)
  elsif locator.is_a? ActsAsFound
    locator.find_food(:lat => self.lat, :lon => self.lon)
  else
    raise NotImplementedError, "#{locator.class.name} is not supported."
  end
end

這是一個比較務實的解決方案。或許咱們也再也不須要考慮去支持另外一個庫了。也或許find_nearest_restaurant就是咱們使用locator的惟一場景。設計模式

那假如你真的須要去支持一個新的locator,那又會是怎麼樣的呢?那就是你有三個特定的場景。再假如你須要實現find_nearest_hospital方法呢?這樣你就須要在維護這三種特定的場景時去兼顧兩個不一樣的地方。當你以爲這種解決方案再也不可行的時候,你就須要考慮適配器模式了。ruby

在這個例子中,咱們能夠爲GeoFish以及ActsAsFound編寫適配器,這樣的話,在咱們的其餘代碼中,咱們就不須要了解咱們當前正在使用的是哪一個庫了:編程語言

def find_nearest_hospital(locator)
  locator.find :type => :hospital,
               :lat => self.lat,
               :lon => self.lon
end

locator = GeoFishAdapter.new(geo_fish_locator)
find_nearest_hospital(locator)

特地假設的例子就到此爲止,接下來讓咱們看看真實的代碼。學習

MultiJSON

ActiveSupport在作JSON格式的解碼時,用到的是MultiJSON,這是一個針對JSON庫的適配器。每個庫都可以解析JSON,可是作法卻不盡相同。讓咱們分別看看針對ojyajl的適配器。
(提示: 可在命令行中輸入qw multi_json查看源碼。)

module MultiJson
  module Adapters
    class Oj < Adapter
      #...
      def load(string, options={})
        options[:symbol_keys] = options.delete(:symbolize_keys)
        ::Oj.load(string, options)
      end
      #...

Oj的適配器修改了options哈希表,使用Hash#delete:symbolize_keys項轉換爲Oj的:symbol_keys項:

options = {:symbolize_keys => true}
options[:symbol_keys] = options.delete(:symbolize_keys) # => true
options                                                 # => {:symbol_keys=>true}

接下來MultiJSON調用了::Oj.load(string, options)。MultiJSON適配後的API跟Oj原有的API很是類似,在此沒必要贅述。不過你是否注意到,Oj是如何引用的呢?::Oj引用了頂層的Oj類,而不是MultiJson::Adapters::Oj

如今讓咱們看看MultiJSON又是如何適配Yajl庫的:

module MultiJson
  module Adapters
    class Yajl < Adapter
      #...
      def load(string, options={})
        ::Yajl::Parser.new(:symbolize_keys => options[:symbolize_keys]).parse(string)
      end
      #...

這個適配器從不一樣的方式實現了load方法。Yajl的方式是先建立一個解析器的實力,而後將傳入的字符串string做爲參數調用Yajl::Parser#parse方法。在options哈希表上的處理也略有不一樣。只有:symbolize_keys項被傳遞給了Yajl。

這些JSON的適配器看似微不足道,可是他們卻可讓你爲所欲爲地在不一樣的庫之間進行切換,而不須要在每個解析JSON的地方更新代碼。

ActiveRecord

不少JSON庫每每都聽從類似的模式,這讓適配工做變得至關輕鬆。可是若是你是在處理一些更加複雜的狀況時,結果會是怎樣?ActiveRecord包含了針對不一樣數據庫的適配器。儘管PostgreSQL和MySQL都是SQL數據庫,可是他們之間仍是有不少不一樣之處,而ActiveRecord經過使用適配器模式屏蔽了這些不一樣。(提示: 命令行中輸入qw activerecord查看ActiveRecord的代碼)

打開ActiveRecord代碼庫中的lib/connection_adapters目錄,裏邊會有針對PostgreSQL,MySQL以及SQLite的適配器。除此以外,還有一個名爲AbstractAdapter的適配器,它做爲每個具體的適配器的基類。AbstractAdapter實現了在大部分數據庫中常見的功能,這些功能在其子類好比PostgreSQLAdapter以及AbstractMysqlAdapter中被從新定製,而其中AbstractMysqlAdapter則是另外兩個不一樣的MySQL適配器——MysqlAdapter以及Mysql2Adapter——的父類。讓咱們經過一些真實世界中的例子來看看他們是如何一塊兒工做的。

PostgreSQL和MySQL在SQL方言的實現稍有不一樣。查詢語句SELECT * FROM users在這兩個數據庫均可以正常執行,可是它們在一些類型的處理上會稍顯不一樣。在MySQL和PostgreSQL中,時間格式就不盡相同。其中,PostgreSQL支持微秒級別的時間,而MySQL只是到了最近的一個穩定發佈的版本中才支持。那這兩個適配器又是如何處理這種差別的呢?

ActiveRecord經過被混入到AbstractAdapterActiveRecord::ConnectionAdapters::Quoting中的quoted_date引用日期。而AbstractAdapter中的實現僅僅只是格式化了日期:

def quoted_date(value)
  #...
  value.to_s(:db)
end

Rails中的ActiveSupport擴展了Time#to_s,使其可以接收一個表明格式名的符號類型參數。:db所表明的格式就是%Y-%m-%d %H:%M:%S

# Examples of common formats:
Time.now.to_s(:db)      #=> "2014-02-19 06:08:13"
Time.now.to_s(:short)   #=> "19 Feb 06:08"
Time.now.to_s(:rfc822)  #=> "Wed, 19 Feb 2014 06:08:13 +0000"

MySQL的適配器都沒有重寫quoted_date方法,它們天然會繼承這種行爲。另外一邊,PostgreSQLAdapter則對日期的處理作了兩個修改:

def quoted_date(value)
  result = super
  if value.acts_like?(:time) && value.respond_to?(:usec)
    result = "#{result}.#{sprintf("%06d", value.usec)}"
  end

  if value.year < 0
    result = result.sub(/^-/, "") + " BC"
  end
  result
end

它在一開始便調用super方法,因此它也會獲得一個相似MySQL中格式化後的日期。接下來,它檢測value是否像是一個具體時間。這是一個ActiveSupport中擴展的方法,當一個對象相似Time類型的實例時,它會返回true。這讓它更容易代表各類對象已被假設爲相似Time的對象。(提示: 對acts_like?方法感興趣?請在命令行中執行qw activesupport,而後閱讀core_ext/object/acts_like.rb

第二部分的條件檢查value是否有用於返回毫秒的usec方法。若是能夠求得毫秒數,那麼它將經過sprintf方法被追加到result字符串的末尾。跟不少時間格式同樣,sprintf也有不少不一樣的方式用於格式化數字:

sprintf("%06d", 32) #=> "000032"
sprintf("%6d",  32) #=> "    32"
sprintf("%d",   32) #=> "32"
sprintf("%.2f", 32) #=> "32.00"

最後,假如日期是一個負數,PostgreSQLAdapter就會經過加上"BC"去從新格式化日期,這是PostgreSQL數據庫的實際要求:

SELECT '2000-01-20'::timestamp;
-- 2000-01-20 00:00:00
SELECT '2000-01-20 BC'::timestamp;
-- 2000-01-20 00:00:00 BC
SELECT '-2000-01-20'::timestamp;
-- ERROR:  time zone displacement out of range: "-2000-01-20"

這只是ActiveRecord適配多個API時的一個極小的方式,但它卻能幫助你免除因爲不一樣數據庫的細節所帶來的差別和煩惱。

另外一個體現SQL數據庫的不一樣點是數據庫表被建立的方式。MySQL以及PostgreSQL中對主鍵的處理各不相同:

# AbstractMysqlAdapter
NATIVE_DATABASE_TYPES = {
  :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
  #...
}

# PostgreSQLAdapter
NATIVE_DATABASE_TYPES = {
  primary_key: "serial primary key",
  #...
}

這兩種適配器都可以明白ActiveRecord中的主鍵的表示方式,可是它們會在建立新表的時候將此翻譯爲不一樣的SQL語句。當你下次在編寫一個migration或者執行一個查詢的時候,思考一下ActiveRecord的適配器以及它們爲你作的全部微小的事情。

DateTime和Time

當MultiJson以及ActiveRecord實現了傳統的適配器的時候,Ruby的靈活性使得另外一種解決方案成爲可能。DateTime以及Time都用於表示時間,可是它們在內部的處理上是不一樣的。雖然有着這些細微的差別,可是它們所暴露出來的API倒是極其相似的(提示:命令行中執行qw activesupport查看此處相關代碼):

t = Time.now
t.day     #=> 19         (Day of month)
t.wday    #=> 3          (Day of week)
t.usec    #=> 371552     (Microseconds)
t.to_i    #=> 1392871392 (Epoch secconds)

d = DateTime.now
d.day     #=> 19         (Day of month)
d.wday    #=> 3          (Day of week)
d.usec    #=> NoMethodError: undefined method `usec'
d.to_i    #=> NoMethodError: undefined method `to_i'

ActiveSupport經過添加缺失的方法來直接修改DateTimeTime,進而抹平了二者之間的差別。從實例上看,這裏就有一個例子演示了ActiveSupport如何定義DateTime#to_i

class DateTime
  def to_i
    seconds_since_unix_epoch.to_i
  end

  def seconds_since_unix_epoch
    (jd - 2440588) * 86400 - offset_in_seconds + seconds_since_midnight
  end

  def offset_in_seconds
    (offset * 86400).to_i
  end

  def seconds_since_midnight
    sec + (min * 60) + (hour * 3600)
  end
end

每個用於支持的方法,seconds_since_unix_epochoffset_in_seconds,以及seconds_since_midnight都使用或者擴展了DateTime中已經存在的API去定義與Time中匹配的方法。

假如說咱們前面所看到的適配器是相對於被適配對象的外部適配器,那麼咱們如今所看到的這個就能夠被稱之爲內部適配器。與外部適配器不一樣的是,這種方法受限於已有的API,而且可能致使一些麻煩的矛盾問題。舉例來講,DateTimeTime在一些特殊的場景下就有可能出現不同的行爲:

datetime == time #=> true
datetime + 1     #=> 2014-02-26 07:32:39
time + 1         #=> 2014-02-25 07:32:40

當加上1的時候,DateTime加上了一天,而Time則是加上了一秒。當你須要使用它們的時候,你要記住ActiveSupport基於這些不一樣,提供了諸如changeDuration等保證一致行爲的方法或類。

這是一個好的模式嗎?它理所固然是方便的,可是如你剛纔所見,你仍舊須要注意其中的一些不一樣之處。

總結

設計模式不是隻有Java才須要的。Rails經過使用設計模式以提供用於JSON解析以及數據庫維護的統一接口。因爲Ruby的靈活性,相似DateTime以及Time這樣的類能夠被直接地修改而提供類似的接口。Rails的源碼就是一個可讓你挖掘真實世界中不一樣設計模式實例的天堂。

在此次的實踐中,咱們同時也發掘了一些有趣的代碼:

  • hash[:foo] = hash.delete(:bar)是一個用於重命名哈希表中某一項的巧妙方法。
  • 調用::ClassName會調用頂層的類。
  • ActiveSupport爲TimeDate以及其餘的類添加了一個可選的表明格式的參數format
  • sprintf能夠用於格式化數字。

想要探索更多的知識?回去看看MultiJson是如何處理以及解析格式的。仔細閱讀你在你的數據庫中所使用到的ActiveRecord的適配器的代碼。瀏覽ActiveSupport中用於xml適配器的XmlMini,它跟MultiJson中的JSON適配器是相似的。在這些裏面還會有不少能夠學習的。

相關文章
相關標籤/搜索