Ruby设计模式编程之适配器模式实战攻略

适配器模式
适配器模式可以用于对不同的接口进行包装以及提供统一的接口,或者是让某一个对象看起来像是另一个类型的对象。在静态类型的编程语言里,我们经常使用它去满足类型系统的特点,但是在类似Ruby这样的弱类型编程语言里,我们并不需要这么做。尽管如此,它对于我们来说还是有很多意义的。
当使用第三方类或者库的时候,我们经常从这个例子开始(start out fine):

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

我们假设有一个针对locator的接口,但是如果我们想要find_nearest_restaurant能够支持另一个库呢?这个时候我们可能就会去尝试添加新的特殊的场景的处理:

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方法呢?这样你就需要在维护这三种特定的场景时去兼顾两个不同的地方。当你觉得这种解决方案不再可行的时候,你就需要考虑适配器模式了。
在这个例子中,我们可以为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)

特意假设的例子就到此为止,接下来让我们看看真实的代码。

实例
今天一大早,你的leader就匆匆忙忙跑过来找到你:“快,快,紧急任务!最近ChinaJoy马上就要开始了,老板要求提供一种直观的方式,可以查看到我们新上线的游戏中每个服的在线人数。”
你看了看日期,不是吧!这哪里是马上要开始了,分明是已经开始了!这怎么可能来得及呢?
“没关系的。”你的leader安慰你道:“功能其实很简单的,接口都已经提供好了,你只需要调用一下就行了。”
好吧,你勉为其难地接受了,对于这种突如其来的新需求,你早已习惯。
你的leader向你具体描述了一下需求,你们的游戏目前有三个服,一服已经开放一段时间了,二服和三服都是新开的服。设计的接口非常轻便,你只需要调用Utility.online_player_count(Fixnum),传入每个服对应的数值就可以获取到相应服在线玩家的数量了,如一服传入1,二服传入2,三服则传入3。如果你传入了一个不存在的服,则会返回-1。然后你只要将得到的数据拼装成XML就好,具体的显示功能由你的leader来完成。
好吧,听起来功能并不是很复杂,如果现在就开始动工好像还来得及,于是你马上敲起了代码。
首先定义一个用于统计在线人数的父类PlayerCount,代码如下:

class PlayerCount 

  def server_name
    raise "You should override this method in subclass."
  end 

  def player_count
    raise "You should override this method in subclass."
  end 

end

接着定义三个统计类继承PlayerCount,分别对应了三个不同的服,如下所示:

class ServerOne < PlayerCount 

  def server_name
    "一服"
  end 

  def player_count
    Utility.online_player_count(1)
  end 

end 

class ServerTwo < PlayerCount 

  def server_name
    "二服"
  end 

  def player_count
    Utility.online_player_count(2)
  end 

end 

class ServerThree < PlayerCount 

  def server_name
    "三服"
  end 

  def player_count
    Utility.online_player_count(3)
  end 

end

然后定义一个XMLBuilder类,用于将各服的数据封装成XML格式,代码如下:

class XMLBuilder 

  def self.build_xml player
    builder = ""
    builder << "<root>"
    builder << "<server>" << player.server_name << "</server>"
    builder << "<player_count>" << player.player_count.to_s << "</player_count>"
    builder << "</root>"
  end 

end

这样的话,所有代码就完工了,如果你想查看一服在线玩家数只需要调用:

XMLBuilder.build_xml(ServerOne.new)

查看二服在线玩家数只需要调用:

XMLBuilder.build_xml(ServerTwo.new)

查看三服在线玩家数只需要调用:

XMLBuilder.build_xml(ServerThree.new)

咦?你发现查看一服在线玩家数的时候,返回值永远是-1,查看二服和三服都很正常。
你只好把你的leader叫了过来:“我感觉我写的代码没有问题,但是查询一服在线玩家数总是返回-1,为什么会这样呢?”
“哎呀!”你的leader猛然想起,“这是我的问题,前面没跟你解释清楚。由于我们的一服已经开放一段时间了,查询在线玩家数量的功能早就有了,使用的是ServerFirst这个类。当时写Utility.online_player_count()这个方法主要是为了针对新开的二服和三服,就没把一服的查询功能再重复做一遍。这种情况下可以使用适配器模式,这个模式就是为了解决接口之间不兼容的问题而出现的。”
其实适配器模式的使用非常简单,核心思想就是只要能让两个互不兼容的接口能正常对接就行了。上面的代码中,XMLBuilder中使用PlayerCount来拼装XML,而ServerFirst并没有继承PlayerCount,这个时候就需要一个适配器类来为XMLBuilder和ServerFirst之间搭起一座桥梁,毫无疑问,ServerOne就将充当适配器类的角色。修改ServerOne的代码,如下所示:

class ServerOne < PlayerCount 

  def initialize
    @serverFirst = ServerFirst.new
  end 

  def server_name
    "一服"
  end 

  def player_count
    @serverFirst.online_player_count
  end 

end 

这样通过ServerOne的适配,XMLBuilder和ServerFirst之间就成功完成对接了!使用的时候我们甚至无需知道有ServerFirst这个类,只需要正常创建ServerOne的实例就行了。
需要值得注意的一点是,适配器模式不并是那种会让架构变得更合理的模式,更多的时候它只是充当救火队员的角色,帮助解决由于前期架构设计不合理导致的接口不匹配的问题。更好的做法是在设计的时候就尽量把以后可能出现的情况多考虑一些,在这个问题上不要向你的leader学习。

MultiJSON
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的地方更新代码。
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通过被混入到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的适配器以及它们为你做的所有微小的事情。
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通过添加缺失的方法来直接修改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会调用顶层的类。
  • ActiveSupport为Time、Date以及其他的类添加了一个可选的代表格式的参数format。
  • sprintf可以用于格式化数字。

想要探索更多的知识?回去看看MultiJson是如何处理以及解析格式的。仔细阅读你在你的数据库中所使用到的ActiveRecord的适配器的代码。浏览ActiveSupport中用于xml适配器的XmlMini,它跟MultiJson中的JSON适配器是类似的。在这些里面还会有很多可以学习的。

(0)

相关推荐

  • 详解Ruby设计模式编程中对单例模式的运用

    简介       单例模式是设计模式中最简单的形式之一.这一模式的目的是使得类的一个对象成为系统中的唯一实例.要实现这一点,可以从客户端对其进行实例化开始.因此需要用一种只允许生成对象类的唯一实例的机制,"阻止"所有想要生成对象的访问.使用工厂方法来限制实例化过程.这个方法应该是静态方法(类方法),因为让类的实例去生成另一个唯一实例毫无意义. 要点       显然单例模式的要点有三个:一是某个类只能有一个实例:二是它必须自行创建这个实例:三是它必须自行向整个系统提供这个实例.    

  • Ruby设计模式编程中对外观模式的应用实例分析

    何为外观模式? 外观模式为子系统中一组不同的接口提供统一的接口.外观定义了上层接口,通过降低复杂度和隐藏子系统间的通信以及依存关系,让子系统更加易于使用. 比方说子系统中有一组不同的类,其中一些彼此依赖.这让客户端难以使用子系统中的类,因为客户端需要知道每一个类.外观起到整个子系统的入口.有些客户端只需要子系统的某些基本行为,而对子系统的类不做太多定制,外观为这样的客户端提供简化的接口.只有需要从某些子系统的类定制更多行为的客户端,才会关注外观背后的细节. 外观模式:为系统中的一组接口提供一个统

  • 深入剖析Ruby设计模式编程中对命令模式的相关使用

    命令模式是对象行为型使用率比较高的设计模式,别名:Action(动作),Transaction(事务) 意图: 将一个请求封装为一个对象,从而使你可对不同的请求进行参数化:对请求排队或记录请求日志,以及支持可取消的操作 这里所谓的"不同的请求"也既意味着请求可能发生的变化,是一个可能扩展的功能点. 动机: 方便扩展 结构: 协作说明:    参与角色:     Command 声明一个接口以用来实现某个操作.     ConcreteCommand 将动作与Reciver对外绑定,通过

  • Ruby中使用设计模式中的简单工厂模式和工厂方法模式

    之前有看过<ruby设计模式>,不过渐渐的都忘记了.现在买了一个大话设计模式,看起来不是那么枯燥,顺便将代码用ruby实现了一下. 简单工厂模式: # -*- encoding: utf-8 -*- #运算类 class Operation attr_accessor :number_a,:number_b def initialize(number_a = nil, number_b = nil) @number_a = number_a @number_b = number_b end d

  • 实例讲解Ruby使用设计模式中的装饰器模式的方法

    概述        若你从事过面向对象开发,实现给一个类或对象增加行为,使用继承机制,这是所有面向对象语言的一  个基本特性.如果已经存在的一个类缺少某些方法,或者须要给方法添加更多的功能(魅力),你也许会仅仅继承这个类来产生一个新类-这建立在额外的代码上.       通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法.但是这种方法是静态的,用户不能控制增加行为的方式和时机.如果  你希望改变一个已经初始化的对象的行为,你怎么办?或者,你希望继承许多类的行为,改怎么办?前一个,

  • 设计模式中的观察者模式在Ruby编程中的运用实例解析

    观察者模式(有时又被称为发布/订阅模式)是软件设计模式的一种. 在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知. 这通常透过呼叫各观察者所提供的方法来实现. 实现观察者模式的时候要注意,观察者和被观察对象之间的互动关系不能 体现成类之间的直接调用,否则就将使观察者和被观察对象之间紧密的耦合起来, 从根本上违反面向对象的设计的原则.无论是观察者"观察"观察对象, 还是被观察者将自己的改变"通知"观察者,都不应该直接调用.

  • Ruby设计模式编程中使用Builder建造者模式的实例

    先来复习一下设计模式的基本概念: 定义 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示. 建造者隐藏了该产品是如何组装的,所以若需要改变一个产品的内部表示,只需要重新定一个建造者就可以了. 实用范围 1.当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时. 2.当构造过程必须允许被构造的对象有不同表示时. 角色 在这样的设计模式中,有以下几个角色: 1.builder:为创建一个产品对象的各个部件指定抽象接口. 2.ConcreteBuilder:实现B

  • 实例解析Ruby设计模式开发中对观察者模式的实现

    一般来说,观察者模式的定义应该是这样的:building a clean interface between the source of news that some object has changed and the consumers of that news. 观察者模式在消息的生产者和消费者之间建立了clean interface,这样就使得消息的生产者和消费者之间的耦合是抽象的.被观察者可以不认识任何一个的观察者,它只知道他们都实现了一个共同的接口.由于观察者和被观察者没有紧密的耦合

  • 详解组合模式的结构及其在Ruby设计模式编程中的运用

    定义:也叫合成模式,或者部分-整体模式,主要是用来描述部分与整体的关系,定义,将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性. 类图: 角色说明: Componnent抽象构件角色:定义参加组合对象的共有方法和属性,可以定义一些默认的行为或属性. Leaf叶子构件:叶子对象,其下再也没有其他的分支,也就是遍历的最小单位. Composite树枝构件:树枝对象,它的作用是组合树枝节点和叶子节点形成一个树形结构. 实例: 听说你们公司最近新

  • 解析proxy代理模式在Ruby设计模式开发中的运用

    代理模式 Proxy代理模式是一种结构型设计模式,主要解决的问题是:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上.在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层.如下图: 比如说C和A不在一个服务器上,A要频繁的调用C,我们可以在A上做一个代理类Proxy,把访问C的工作交给Proxy,这样对于A来说,就好像在直接访问C的对

  • Ruby使用设计模式中的代理模式与装饰模式的代码实例

    代理模式 需求: 小明让小李替他追小丽(送洋娃娃,送花,送巧克力) 没有代理的代码: # -*- encoding: utf-8 -*- #追求者类 class Pursuit attr_accessor :mm def initialize(mm) @mm = mm end def give_dolls puts "#{mm.name} 送你洋娃娃" end def give_flowers puts "#{mm.name} 送你鲜花" end def give_

  • 实例解析Ruby设计模式编程中Strategy策略模式的使用

    今天你的leader兴致冲冲地找到你,希望你可以帮他一个小忙,他现在急着要去开会.要帮什么忙呢?你很好奇. 他对你说,当前你们项目的数据库中有一张用户信息表,里面存放了很用户的数据,现在需要完成一个选择性查询用户信息的功能.他说会传递给你一个包含许多用户名的数组,你需要根据这些用户名把他们相应的数据都给查出来. 这个功能很简单的嘛,你爽快地答应了.由于你们项目使用的是MySQL数据库,你很快地写出了如下代码: require 'mysql' class QueryUtil def find_us

  • 设计模式中的模板方法模式在Ruby中的应用实例两则

    实例一 今天你还是像往常一样来上班,一如既往地开始了你的编程工作. 项目经理告诉你,今天想在服务器端增加一个新功能,希望写一个方法,能对Book对象进行处理,将Book对象的所有字段以XML格式进行包装,这样以后可以方便与客户端进行交互.并且在包装开始前和结束后要打印日志,这样方便调试和问题定位. 没问题!你觉得这个功能简直是小菜一碟,非常自信地开始写起代码. Book对象代码如下: class Book attr_accessor :book_name, :pages, :price, :au

随机推荐