Introduction to the adapter mode of Ruby Design Mode Programming, and the ruby Design Mode
Adapter Mode
The adapter mode can be used to encapsulate different interfaces and provide unified interfaces, or make an object look like another type of object. In a static programming language, we often use it to satisfy the characteristics of a type system, but in a weak programming language like Ruby, we do not need to do this. However, it makes a lot of sense for us.
When using third-party classes or libraries, we often start from this example (start out fine ):
def find_nearest_restaurant(locator) locator.nearest(:restaurant, self.lat, self.lon)end
Suppose we have an interface for locator, but what if we want find_nearest_restaurant to support another library? In this case, we may try to add a new special scenario:
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." endend
This is a more pragmatic solution. Maybe we no longer need to consider supporting another library. Maybe find_nearest_restaurant is the only scenario in which we use locator.
What if you really need to support a new locator? That is, you have three specific scenarios. What if you need to implement the find_nearest_hospital method? In this way, you need to consider two different aspects when maintaining these three specific scenarios. When you think this solution is no longer feasible, you need to consider the adapter mode.
In this example, we can write an adapter for GeoFish and ActsAsFound. In this way, in other code, we do not need to know which library we are currently using:
def find_nearest_hospital(locator) locator.find :type => :hospital, :lat => self.lat, :lon => self.lonendlocator = GeoFishAdapter.new(geo_fish_locator)find_nearest_hospital(locator)
Let's take a look at the real code.
Instance
Early in the morning, your leader rushed to find you: "Fast, fast, urgent task! ChinaJoy is about to begin recently. The boss asked for an intuitive way to view the number of online users of each server in our new game ."
You looked at the date, right! This is where we are about to start. It is clear that we have already started! How can this be done?
"It doesn't matter ." Your leader comforted you: "The function is actually very simple. All interfaces are provided. You just need to call it ."
Well, you accept it for a while. You have long been used to this sudden new demand.
Your leader gave you a detailed description of the requirements. Your game currently has three servers, one server has been open for a while, and the other two and the third servers are all new servers. The designed interface is very lightweight. You only need to call Utility. online_player_count (Fixnum): input the value corresponding to each server to obtain the number of online players in the corresponding server. For example, input 1 to server 2, input 2 to Server 3, and input 3 to Server 3. If you input a server that does not exist,-1 is returned. Then you just need to assemble the data into XML, and the specific display function is completed by your leader.
Well, it sounds like the feature is not very complicated. If it seems that it is still time to start the work now, you will immediately start typing the code.
First, define a PlayerCount parent class used to count the number of online users. The Code is as follows:
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
Then define three statistical classes to inherit PlayerCount, which correspond to three different servers, as shown below:
Class ServerOne <PlayerCount def server_name "1 server" end def player_count Utility. online_player_count (1) end class ServerTwo <PlayerCount def server_name "two servers" end def player_count Utility. online_player_count (2) end class ServerThree <PlayerCount def server_name "three servers" end def player_count Utility. online_player_count (3) end
Define an XMLBuilder class to encapsulate data of different servers into XML format. The Code is as follows:
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
In this way, all the code is complete. To view the number of online players in a server, you only need to call:
XMLBuilder.build_xml(ServerOne.new)
To view the number of online gamers in Server 2, you only need to call:
XMLBuilder.build_xml(ServerTwo.new)
To view the number of online players in three servers, you only need to call:
XMLBuilder.build_xml(ServerThree.new)
Why? When you view the number of online players in a server, the returned value is always-1. It is normal to view both Server 2 and Server 3.
You have to call your leader: "I feel that the code I wrote is okay, but the number of online players in a server always returns-1. Why ?"
"Oh !" Your leader suddenly remembered, "This is my problem, and I have not explained it clearly before. Since our server has been open for a while, the function of querying the number of online players has long been available, using the ServerFirst class. At that time, the Utility. online_player_count () method was mainly used to repeat the query function of a server for the newly opened Server 2 and Server 3. In this case, you can use the adapter mode, which is used to solve the problem of incompatibility between interfaces ."
In fact, the use of the adapter mode is very simple, the core idea is that as long as two incompatible interfaces can be normally connected. In the above Code, PlayerCount is used in XMLBuilder to assemble XML, while ServerFirst does not inherit PlayerCount. In this case, an adapter class is required to build a bridge between XMLBuilder and ServerFirst, serverOne will undoubtedly act as the adapter class. Modify the ServerOne Code as follows:
Class ServerOne <PlayerCount def initialize @ serverFirst = ServerFirst. new end def server_name "server" end def player_count @ serverFirst. online_player_count end
Through ServerOne adaptation, the interaction between XMLBuilder and ServerFirst is successful! We don't even need to know the ServerFirst class when using it. We just need to create a ServerOne instance.
It is worth noting that the adapter mode is not the one that makes the architecture more reasonable. More often, it is only used as a fireman, it helps solve interface mismatch problems caused by unreasonable preliminary architecture design. A better way is to try to consider the possible situations in the future during the design. Do not learn from your leader on this issue.
MultiJSON
ActiveSupport uses MultiJSON for JSON Format Decoding, which is an adapter for the JSON library. Every database can parse JSON, but the practices are different. Let's look at the adapters for oj and yajl respectively. (Note: You can enter qw multi_json in the command line to view the source code .)
module MultiJson module Adapters class Oj < Adapter #... def load(string, options={}) options[:symbol_keys] = options.delete(:symbolize_keys) ::Oj.load(string, options) end #...
The Oj adapter modifies the options Hash table and uses Hash # delete to convert the symbolize_keys item to the symbol_keys item of Oj:
options = {:symbolize_keys => true}options[:symbol_keys] = options.delete(:symbolize_keys) # => trueoptions # => {:symbol_keys=>true}
Next, MultiJSON is called: Oj. load (string, options ). The MultiJSON-adapted API is very similar to the original Oj API, so you do not need to repeat it here. But have you noticed how Oj references it? : Oj references the top-level Oj class, instead of MultiJson: Adapters: Oj.
Now let's take a look at how MultiJSON fits the Yajl Library:
module MultiJson module Adapters class Yajl < Adapter #... def load(string, options={}) ::Yajl::Parser.new(:symbolize_keys => options[:symbolize_keys]).parse(string) end #...
This adapter implements the load method in different ways. The method of Yajl is to first create a Parser strength, and then call the Yajl: Parser # parse method using the input string as the parameter. The processing of the options hash table is also slightly different. Only the symbolize_keys item is passed to Yajl.
These JSON adapters seem insignificant, but they allow you to switch between different libraries as you like, without updating the Code in every place where JSON is parsed.
ActiveRecord
Many JSON libraries tend to follow similar models, making adaptation quite easy. But what will happen if you are dealing with more complex situations? ActiveRecord contains adapters for different databases. Although PostgreSQL and MySQL are both sqldatabases, there are still many differences between them, and ActiveRecord shields these differences by using the adapter mode. (Tip: Enter qw activerecord in the command line to view the code of ActiveRecord)
Open the lib/connection_adapters directory in the ActiveRecord code base. There will be adapters for PostgreSQL, MySQL, and SQLite. In addition, there is an adapter named AbstractAdapter, which serves as the base class for each specific adapter. AbstractAdapter implements common functions in most databases. These functions are customized in their subclasses, such as PostgreSQLAdapter and AbstractMysqlAdapter, specifically, AbstractMysqlAdapter is the parent class of two different MySQL adapters, MysqlAdapter and Mysql2Adapter. Let's take some real-world examples to see how they work together.
PostgreSQL and MySQL have different implementations in the SQL dialect. The query statement SELECT * FROM users can be executed normally in both databases, but they are slightly different in some types of processing. In MySQL and PostgreSQL, the time format is different. Among them, PostgreSQL supports the time in microseconds, and MySQL is only supported in a recent stable release version. Then how do these two adapters deal with this difference?
ActiveRecord uses the quoted_date in ActiveRecord: ConnectionAdapters: Quoting to reference the date. The implementation in AbstractAdapter only formats the date:
def quoted_date(value) #... value.to_s(:db)end
In Rails, ActiveSupport extends Time # to_s so that it can receive a symbolic parameter representing the format name. : Db represents % 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 adapters do not override the quoted_date method. They naturally inherit this behavior. On the other side, PostgreSQLAdapter makes two changes to date processing:
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 resultend
It calls the super method at the beginning, so it will also get a date similar to that formatted in MySQL. Next, it checks whether the value is like a specific time. This is an extended method in ActiveSupport. When an instance with an object similar to the Time type is returned, true is returned. This makes it easier to indicate that various objects have been assumed to be objects similar to Time. (Note: For acts_like? Method interest? Execute qw activesupport in the command line and read core_ext/object/acts_like.rb)
The second part checks whether the value has a usec method that returns milliseconds. If the number of milliseconds can be obtained, it will be appended to the end of the result string through the sprintf method. Like many time formats, sprintf also has many different ways to format numbers:
sprintf("%06d", 32) #=> "000032"sprintf("%6d", 32) #=> " 32"sprintf("%d", 32) #=> "32"sprintf("%.2f", 32) #=> "32.00"
Finally, if the date is a negative number, PostgreSQLAdapter will reformat the date by adding "BC". This is the actual requirement of the PostgreSQL database:
SELECT '2000-01-20'::timestamp;-- 2000-01-20 00:00:00SELECT '2000-01-20 BC'::timestamp;-- 2000-01-20 00:00:00 BCSELECT '-2000-01-20'::timestamp;-- ERROR: time zone displacement out of range: "-2000-01-20"
This is only a very small way for ActiveRecord to adapt to multiple APIs, but it can help you avoid the differences and troubles caused by the details of different databases.
Another difference that reflects the sqldatabase is that the database table is created. The primary keys in MySQL and PostgreSQL are processed differently:
# AbstractMysqlAdapterNATIVE_DATABASE_TYPES = { :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", #...}# PostgreSQLAdapterNATIVE_DATABASE_TYPES = { primary_key: "serial primary key", #...}
Both adapters can understand the expression of the primary key in ActiveRecord, but they will translate it into different SQL statements when creating a new table. When you write a migration statement or execute a query, think about the ActiveRecord adapter and all the tiny things they do for you.
DateTime and Time
When MultiJson and ActiveRecord implement traditional adapters, Ruby's flexibility makes another solution possible. DateTime and Time are used to indicate Time, but they are different in internal processing. Despite these minor differences, the APIS they expose are extremely similar (Note: Run qw activesupport in the command line to view the relevant code ):
t = Time.nowt.day #=> 19 (Day of month)t.wday #=> 3 (Day of week)t.usec #=> 371552 (Microseconds)t.to_i #=> 1392871392 (Epoch secconds)d = DateTime.nowd.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 directly modifies DateTime and Time by adding missing methods, thus smoothing the difference between the two. From the instance perspective, here is an example to demonstrate how ActiveSupport defines 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) endend
Every supported method, seconds_since_unix_epoch, offset_in_seconds, and seconds_since_midnight both use or extend the existing APIs in DateTime to define matching methods in Time.
If the adapter we see above is an external adapter relative to the adapted object, then what we see now can be called an internal adapter. Unlike external adapters, this method is restricted by existing APIs and may cause some troublesome conflicts. For example, DateTime and Time may have different behaviors in some special scenarios:
datetime == time #=> truedatetime + 1 #=> 2014-02-26 07:32:39time + 1 #=> 2014-02-25 07:32:40
When 1 is added, DateTime is added with one day, while Time is added with one second. When you need to use them, remember that based on these differences, ActiveSupport provides methods or classes such as change and Duration to ensure consistent behavior.
Is this a good model? It is convenient, but as you have just seen, you still need to pay attention to some of the differences.
Summary
The design pattern is not only required by Java. Rails provides a unified interface for JSON parsing and database maintenance by using the design mode. Due to Ruby's flexibility, classes like DateTime and Time can be directly modified to provide similar interfaces. Rails source code is a paradise that allows you to explore different design patterns in the real world.
In this practice, we also found some interesting code:
- Hash [: foo] = hash. delete (: bar) is a clever method used to rename a hash table.
- Call: ClassName will call the top-level class.
- ActiveSupport adds an optional format parameter format for Time, Date, and other classes.
- Sprintf can be used to format numbers.
Want to explore more knowledge? Go back and check how MultiJson is processed and the parsing format. Read carefully the code of the ActiveRecord adapter you use in your database. Browse the XmlMini used for the xml adapter in ActiveSupport, which is similar to the JSON adapter in MultiJson. There will be a lot to learn in these cases.
Articles you may be interested in:
- Detailed description of the structure of the Combination Mode and Its Application in Ruby Design Mode Programming
- The template method in the design mode applies two examples in Ruby.
- Example parsing: Use of Strategy Mode in Ruby Design Mode Programming
- The example explains how Ruby uses the decorator mode in the design mode.
- Example of using Builder mode in Ruby Design Mode Programming
- Detailed description of the Application of Singleton mode in Ruby Design Mode Programming
- Ruby uses code instances in the proxy mode and decoration mode in the Design Mode
- Ruby uses the simple factory mode and factory method mode in the Design Mode
- Application instance analysis of appearance mode in Ruby Design Mode Programming