Ruby设计模式编程之适配器模式实战攻略
适配器模式 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.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的唯一场景。 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) 特意假设的例子就到此为止,接下来让我们看看真实的代码。 实例 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,查看二服和三服都很正常。 class ServerOne < PlayerCount def initialize @serverFirst = ServerFirst.new end def server_name "一服" end def player_count @serverFirst.online_player_count end end MultiJSON 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。 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。 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) 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时的一个极小的方式,但它却能帮助你免除由于不同数据库的细节所带来的差异和烦恼。 # 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的适配器以及它们为你做的所有微小的事情。 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中匹配的方法。 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等保证一致行为的方法或类。
想要探索更多的知识?回去看看MultiJson是如何处理以及解析格式的。仔细阅读你在你的数据库中所使用到的ActiveRecord的适配器的代码。浏览ActiveSupport中用于xml适配器的XmlMini,它跟MultiJson中的JSON适配器是类似的。在这些里面还会有很多可以学习的。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |