本文翻译自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适配器是相似的。在这些里面还会有不少能够学习的。