Ruby元編程之夢(mèng)中情人method_missing方法詳解
我最近讀了些文章(比如這篇),宣傳在 Ruby 里使用 method_missing 的。
很多人都與 method_missing 干柴烈火,但在并沒有小心處理彼此之間的關(guān)系。所以,我想來(lái)探討一下這個(gè)問(wèn)題:
** 我該怎么用 method_missing **
什么時(shí)候該抵擋 method_missing 的誘惑
首先,永遠(yuǎn)不要在還沒花時(shí)間考慮你用得夠不夠好之前,就向 method_missing 的魅力屈服。你知道,在日常生活中,很少會(huì)讓你以為的那樣亟需 method_missing:
日常:方法代理
案例:我需要讓這個(gè)類能夠使用另一個(gè)類的方法
這是我所見過(guò)最普遍的使用 method_missing 的情況。這在 gems 與 Rails 插件里頭尤其流行。它的模型類似這樣:
class A
def hi
puts "Hi from #{self.class}"
end
end
class B
def initialize
@b = A.new
end
def method_missing(method_name, *args, &block)
@b.send(method_name, *args, &block)
end
end
A.new.hi #=> Hi from A
B.new.hi #=> Hi from A
如此,B 就擁有了 A 的所有實(shí)例方法。但是讓我們想想,在調(diào)用 @b.hi 的時(shí)候都發(fā)生了什么。你的 ruby 環(huán)境沿著繼承鏈一路找 hi 這個(gè)方法,到最后,恰恰在丟出個(gè) NoMethodError 前,它調(diào)了 method_missing 這個(gè)方法。
在上例中,情況并不壞,畢竟這里就兩個(gè)微不足道的類需要查。但通常,我們是在 Rails 或者其他一些框架的上下文中編程。而你的 Rails 模型繼承自 ActiveRecord,而它又集成自其他一大坨的類,于是現(xiàn)在你就有了一坨高高的堆棧要爬?? 在你每次調(diào)用 @b.hi 的時(shí)候!
你的好朋友:define_method
它允許你動(dòng)態(tài)地定義一個(gè)方法(顧名思義)。它的偉大之處在于,在它執(zhí)行過(guò)之后(通常在你的類們加載之后),這些方法就存在你的類中了,簡(jiǎn)單直接。在你創(chuàng)建這些方法的時(shí)候,也沒有什么繼承鏈需要爬。
define_method 很有愛很可靠,并且能夠滿足你的日常生活。不信我?接著看??
class B
define_method(:hi) do
@b.hi
end
end
“可是我有一大坨方法要定義!” 你抱怨
“沒問(wèn)題!” 我賣萌眨眼
class B
[:hi, :bye, :achoo, :gesundheit].each do |name|
define_method(name) do
@b.send(name)
end
end
end
可是我懶得把它們一個(gè)個(gè)寫出來(lái)!
你有點(diǎn)難搞哦
class A
# ... lots of methods in here
end
class B
A.instance_methods.each do |name|
define_method(name) do
@b.send(name)
end
end
end
那假如我要定義的方法跟原本的有那么一些些不一樣呢?
容易
class A
def hi
puts "Hi."
end
end
class B
A.instance_methods.each do |name|
define_method("what_is_#{name}") do
if @b.respond_to?(name)
@b.send(name)
else
false
end
end
end
end
B.new.what_is_hi #=> "Hi."
B.new.what_is_wtf #=> false
呃,代碼看起來(lái)不優(yōu)雅啊
那就沒辦法了,湊合得了。如果你想要代碼更易讀,可以看看我們的ruby delegation library 和 Rails ActiveRecord delegation。
好,我們總結(jié)一下,看看 define_method 的真正威力。
修改自 ruby-doc.org 上的 例子
class A
def fred
puts "In Fred"
end
def create_method(name, &block)
self.class.send(:define_method, name, &block)
end
define_method(:wilma) { puts "Charge it!" }
end
class B < A
define_method(:barney, instance_method(:fred))
end
a = B.new
a.barney #=> In Fred
a.wilma #=> Charge it!
a.create_method(:betty) { p self.to_s }
a.betty #=> B
什么時(shí)候用 method_missing?
現(xiàn)在你估計(jì)在想,總有該用它的時(shí)候吧,不然還要它干嘛?沒錯(cuò)。
動(dòng)態(tài)命名的方法(又名,元方法)
案例:我要依據(jù)某種模式提供一組方法。這些方法做的事情顧名思義。我可能從來(lái)沒有調(diào)用過(guò)這些可能的方法,但是等我要用的時(shí)候,它們必須可用。
現(xiàn)在才是人話!這其實(shí)正是 ActiveRecord 所采用的方式,為你提供那些基于屬性的動(dòng)態(tài)構(gòu)建的查找方法,比如 find_by_login_and_email(user_login, user_email)。
def method_missing(method_id, *arguments, &block)
if match = DynamicFinderMatch.match(method_id)
attribute_names = match.attribute_names
super unless all_attributes_exists?(attribute_names)
if match.finder?
# ...you get the point
end # my OCD makes me unable to omit this
# ...
else
super # this is important, I'll tell you why in a second
end
end
權(quán)衡利弊
當(dāng)你有一大堆元方法要定義,又不一定用得到的時(shí)候,method_missing 是個(gè)完美的折衷。
想想 ActiveRecord 中基于屬性的查找方法。要用 define_method 從頭到腳定義這些方法,ActiveRecord 需要檢查每個(gè)模型的表中所有的字段,并為每個(gè)可能的字段組合方式都定義方法。
find_by_email
find_by_login
find_by_name
find_by_id
find_by_email_and_login
find_by_email_and_login_and_name
find_by_email_and_name
# ...
假如你的模型有 10 個(gè)字段,那就是 10! (362880)個(gè)查找方法需要定義。想象一下,在你的 Rails 項(xiàng)目跑起來(lái)的時(shí)候,有這么多個(gè)方法需要一次定義掉,而 ruby 環(huán)境還得把它們都放在內(nèi)存里頭。
老虎·伍茲都做不來(lái)的事情。
** 正確的 method_missing 使用方式
(以下簡(jiǎn)要摘譯)
1、先檢查
并不是每次調(diào)用都要處理的,你應(yīng)該先檢查一下這次調(diào)用是否符合你需要添加的元方法的模式:
def method_missing(method_id, *arguments, &block)
if method_id.to_s =~ /^what_is_[\w]+/
# do your thing
end
end
2、包起來(lái)
檢查好了,確實(shí)要處理的,請(qǐng)記得把函數(shù)體包在你的好基友,define_method 里面。如此,下次就不用找情婦了:
def method_missing(method_id, *arguments, &block)
if method_id.to_s =~ /^what_is_[\w]+/
self.class.send :define_method, method_id do
# do your thing
end
self.send(method_id)
end
end
3、擦屁股
自己處理不來(lái)的方法,可能父類有辦法,所以 super 一下:
def method_missing(method_id, *arguments, &block)
if method_id.to_s =~ /^what_is_[\w]+/
self.class.send :define_method, method_id do
# do your thing
end
self.send(method_id)
else
super
end
end
4、昭告天下
def respond_to?(method_id, include_private = false)
if method_id.to_s =~ /^what_is_[\w]+/
true
else
super
end
end
要告訴別人,你的類雖然暫時(shí)還沒有這個(gè)方法,但是其實(shí)是能夠響應(yīng)這方法的。
** 總結(jié) **
在每個(gè) Ruby 程序員的生活中,這仨方法扮演了重要的角色。define_method 是你的好朋友,method_missing 是個(gè)如膠似漆但也需相敬如賓的情人,而 respond_to? 則是你的愛子,如此無(wú)虞。
相關(guān)文章
Ruby的字符串與數(shù)組求最大值的相關(guān)問(wèn)題討論
這篇文章主要介紹了Ruby中的字符串與數(shù)組求最大值的相關(guān)問(wèn)題,文中還提到了sort排序方法的相關(guān)用法,需要的朋友可以參考下2016-03-03在阿里云 (aliyun) 服務(wù)器上搭建Ruby On Rails環(huán)境
最近總是在配置阿里云的服務(wù)器,遇到不少問(wèn)題,現(xiàn)小結(jié)一下,供大家參考~~2014-06-06詳解Ruby中正則表達(dá)式對(duì)字符串的匹配和替換操作
這篇文章主要介紹了Ruby中正則表達(dá)式對(duì)字符串的匹配和替換操作,包括對(duì)結(jié)果分組和一些特殊全局變量的介紹,需要的朋友可以參考下2016-04-04