本文翻譯自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)
特地假設的例子就到此爲止,接下來讓咱們看看真實的代碼。學習
ActiveSupport
在作JSON格式的解碼時,用到的是MultiJSON
,這是一個針對JSON庫的適配器。每個庫都可以解析JSON,可是作法卻不盡相同。讓咱們分別看看針對oj和yajl的適配器。
(提示: 可在命令行中輸入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的地方更新代碼。
不少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經過被混入到AbstractAdapter
的ActiveRecord::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的適配器以及它們爲你作的全部微小的事情。
當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經過添加缺失的方法來直接修改DateTime
和Time
,進而抹平了二者之間的差別。從實例上看,這裏就有一個例子演示了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_epoch
,offset_in_seconds
,以及seconds_since_midnight
都使用或者擴展了DateTime
中已經存在的API去定義與Time
中匹配的方法。
假如說咱們前面所看到的適配器是相對於被適配對象的外部適配器,那麼咱們如今所看到的這個就能夠被稱之爲內部適配器。與外部適配器不一樣的是,這種方法受限於已有的API,而且可能致使一些麻煩的矛盾問題。舉例來講,DateTime
和Time
在一些特殊的場景下就有可能出現不同的行爲:
datetime == time #=> true datetime + 1 #=> 2014-02-26 07:32:39 time + 1 #=> 2014-02-25 07:32:40
當加上1的時候,DateTime
加上了一天,而Time
則是加上了一秒。當你須要使用它們的時候,你要記住ActiveSupport基於這些不一樣,提供了諸如change
和Duration
等保證一致行爲的方法或類。
這是一個好的模式嗎?它理所固然是方便的,可是如你剛纔所見,你仍舊須要注意其中的一些不一樣之處。
設計模式不是隻有Java才須要的。Rails經過使用設計模式以提供用於JSON解析以及數據庫維護的統一接口。因爲Ruby的靈活性,相似DateTime
以及Time
這樣的類能夠被直接地修改而提供類似的接口。Rails的源碼就是一個可讓你挖掘真實世界中不一樣設計模式實例的天堂。
在此次的實踐中,咱們同時也發掘了一些有趣的代碼:
hash[:foo] = hash.delete(:bar)
是一個用於重命名哈希表中某一項的巧妙方法。::ClassName
會調用頂層的類。Time
、Date
以及其餘的類添加了一個可選的表明格式的參數format
。sprintf
能夠用於格式化數字。想要探索更多的知識?回去看看MultiJson是如何處理以及解析格式的。仔細閱讀你在你的數據庫中所使用到的ActiveRecord的適配器的代碼。瀏覽ActiveSupport中用於xml適配器的XmlMini
,它跟MultiJson中的JSON適配器是相似的。在這些裏面還會有不少能夠學習的。