今天我们还是继续来学习 Ruby 的有关知识吧。这篇文章,我想聊聊 Ruby 中的 monkey-patching
和 refine()
、using()
两个方法。
Ruby 中的 monkey-patching
monkey-patching
,中文名 「 猴子补丁 」。猴子补丁的作用就是在运行时动态的替换属性。
对于 Ruby 来说,因为类和模块可以重新打开。因此,对猴子补丁的需求几乎为零。
比如下面的代码,我们将一个哈希表转换为字符串,也就是调用 to_s()
方法
demo.rb
h = {"name":"简单教程,简单编程"} p h.to_s
运行结果如下
[yufei@www.twle.cn java]$ ruby demo.rb "{:name=>\"简单教程,简单编程\"}"
如果我们想要改变 to_s()
方法的结果,很简单,我们只要重新定义这个方法即可,就像下面这样
demo.rb
class Hash def to_s 'hash' end end h = {"name":"简单教程,简单编程"} p h.to_s
运行结果如下
[yufei@www.twle.cn java]$ ruby demo.rb "hash"
在 Ruby 运行时改变属性是如此的容易,以至于猴子补丁都是可有可无的。
这种在运行时给程序打补丁的方法很有用。虽然更可取的方法是使用 里式替换原则。
猴子补丁最大的好处是什么? 可以暂时性的避免程序崩溃!!!
这又是什么原理么?
猴子补丁可以在打补丁的类的所有实例之间共享,比如,在我们上面的范例中,就是所有的哈希表都共享替换后的 to_s()
方法。
要不,我们再举个例子??
这样吧,我就拿 Ruby On Rails 中常用到的将哈希表转换为 JSON 作为例子。
假设我们存在一个模块 lib/my_lib.rb
,这个模块中定义了一个类 MyLib
,这个类包含一个方法 print_config()
会调用类的哈希表的 to_json()
方法将配置转换为 json
lib/my_lib.rb
# lib/my_lib.rb class MyLib attr_accessor :config def initialize config = {} yield(config) if block_given? end def print_config "config: #{config.to_json}" # <= Monkey-patch applied here end end
那现在我们突然改变构架,不希望 print_config()
方法返回 json
。那要怎么做呢?
当然了,最好的办法就是直接修改 print_config()
方法。
但如果我们某些原因改不了 print_config
怎么办呢? 这时候,猴子补丁就派上用场了。
我们可以使用猴子补丁给哈希表打一个补丁,动态改变 to_json()
方法,如下
lib/core_ext/hash.rb
class Hash # 临时猴子补丁 # 禁止 MyLib#print_config 输出配置 def to_json '' end end
但是,这个猴子补丁也会有副作用,假设其它地方也使用了 to_json()
方法,那就不会有任何输出了。
app/controllers/products_controller.rb
class ProductsController < ApplicationController def index @products = { products: [ # list of products ] } render json: @products # <= Side-effect of the monkey-patch end end
这样,将直接导致 GET /products.json
路由可能不会获得任何输出。因为猴子补丁会改变补丁所在类的所有实例。
这个范例,也演示使用 Ruby 的类打开机制直接修补类的一个限制。
为了解决这个问题,在 Ruby 2.0 中,引入了 Refinement 的概念。
refine()
和 using()
方法
refine()
和 using()
方法都在 Module
中定义。
Module#refine()
方法允许我们为特定类注册一个猴子补丁 ( monkey-patch ) ,然后通过调用 Module#using()
方法随时应用它。
这两个方法和上面提到的类重新打开机制最大的不同,就是把猴子补丁的定义和应用拆开。使得只要在特定情况下调用 using()
应用即可。
因此,接下来,我们主要谈论 refinement 机制,而不会猴子补丁。
我们先来看一段代码
demo.rb
module TemporaryPatch refine Hash do def to_s '' end end end my_ebook = { ebook: 'Ruby Object Model', url: 'https://goo.gl/87b1bi', message: 'feel free to have a look to my new project ;-)' } p my_ebook.to_s # => "{\"ebook\":\"Ruby Object Model\", ...}" using TemporaryPatch lolcat = { lol: 'cat' } p lolcat.to_s # => "" p my_ebook.to_s # => ""
运行上面这段代码,输出结果如下
[yufei@www.twle.cn java]$ ruby demo.rb "{:ebook=>\"Ruby Object Model\", :url=>\"https://goo.gl/87b1bi\", :message=>\"feel free to have a look to my new project ;-)\"}" "" ""
上面这段代码中,我们先使用 refine()
方法重新定义 to_s()
方法。定义完了之后呢,它并不会立即应用到 哈希表的所有类中,而是要等到调用 using TemporaryPatch
方法的时候才会应用。
请注意,模块名称并不重要,但最好根据细化的上下文对其进行命名。
因为一旦调用 using TemporaryPatch
方法,那么该补丁就会立即生效,而且会影响之后所有的 to_s()
调用。所以我们看到 p my_ebook.to_s
的输出结果也是空字符串。
注意,因为这种猴子补丁一旦应用,会影响所有实例,因此,总是在调用使用之前已实例化了
my_ebook
哈希,仍然可以看到它的输出结果为""