譯自這篇文章苇倡。這篇文章里涉及到了ruby元編程里的blank slate坦胶,以及method_missing的使用碎连,一定程度上也是有些研究價(jià)值。簡單翻譯一下均函,以備后用
通常意義來說究西,OO編程就是在對(duì)象間傳消息裁僧。當(dāng)然采幌,OO方式也鼓勵(lì)我們使用相對(duì)準(zhǔn)確的名詞跟動(dòng)詞】糠啵可以把它想像成一個(gè)舞臺(tái)劇蜡吧,上面的參與者相互間在交流。有時(shí)占键,一個(gè)角色可能會(huì)通過一個(gè)第三者來與另一個(gè)角色進(jìn)行交流昔善,這種通過一個(gè)中間角色進(jìn)行交流的方式就叫作代理(delegate)。
先通過一個(gè)示例演示一下delegate是如何幫我們?cè)O(shè)計(jì)一個(gè)強(qiáng)壯且可擴(kuò)展的接口畔乙。然后一起看一下delegate.rb
的源碼君仆,來分析它是如何做到的。
給我提供一個(gè)電影的推薦吧
假設(shè)我們?cè)谧鲆粋€(gè)電影推薦的后端。簡化的來看返咱,我們的Movie
的score
的值是從iMDb跟爛番茄上得到的氮帐。假設(shè)它們的精度都是一樣的。我們想要得到一個(gè)值average_score
洛姑,它是兩個(gè)值的平均值。代碼如下:
class Movie
attr_reader :imdb_score, :rotten_tomatoes_score
def initialize(name, imdb_score, rotten_tomatoes_score)
@name = name
@imdb_score = imdb_score
@rotten_tomatoes_score = rotten_tomatoes_score
end
def average_score
(@imdb_score + @rotten_tomatoes_score) / 2
end
end
接下去我們需要一個(gè)類來表示一個(gè)Movie
的集合皮服,就叫它為RecommendedMovies
楞艾,我們可以這樣來進(jìn)行查詢:
class RecommendedMovies
def initialize(movies)
@movies = movies
end
def best_by_imdb
@movies.max_by(&:imdb_score)
end
def best_by_rotten_tomatoes
@movies.max_by(&:rotten_tomatoes_score)
end
def best
@movies.max_by(&:average_score)
end
end
這很直觀。添加一個(gè)測(cè)試代碼:
north_by_northwest = Movie.new('North by Northwest', 85, 100)
inception = Movie.new('Inception', 88, 86)
the_dark_knight = Movie.new('The Dark Knight', 90, 94)
recommended_movies = RecommendedMovies.new([north_by_northwest, inception, the_dark_knight])
可以這樣去查詢:
recommended_movies.best=> #<Movie:0x007fbcf7048948 [@name](http://twitter.com/name)="North by Northwest", [@imdb_score](http://twitter.com/imdb_score)=85, [@rotten_tomatoes_scor](http://twitter.com/rotten_tomatoes_scor)e=100>
有限的責(zé)任
上面這個(gè)類看上去挺好的龄广,但有一個(gè)缺陷:我們是用一個(gè)array去進(jìn)行初始化硫眯,但之后丟失了所有原來Array
具備的行為:如果我們運(yùn)行recommended_movies.count
,會(huì)得到一個(gè)NoMethodError
返回择同。我們有可能會(huì)想用到Array
(以及Enumerable
)里的一些功能两入,但現(xiàn)在都會(huì)報(bào)錯(cuò)。當(dāng)然敲才,我們可以通過實(shí)現(xiàn)method_missing
來解決裹纳,但我們可以使用一種更優(yōu)雅的方式來解決,Ruby標(biāo)準(zhǔn)包里的庫——delegate.rb紧武。
這個(gè)庫里給我們提供了兩種比較具體的解決方法——兩種都是通過繼承 來實(shí)現(xiàn)的剃氧。DelegateClass
值得單獨(dú)再深入研究一下,更簡單的一種方式是使用SimpleDelegator
阻星,它已經(jīng)能滿足我們上面的需求了朋鞍。我們可以這樣使用:
require 'delegate'
class RecommendedMovies < SimpleDelegator
def best_by_imdb
max_by(&:imdb_score)
end
def best_by_rotten_tomatoes
max_by(&:rotten_tomatoes_score)
end
def best
max_by(&:average_score)
end
end
譯注:跟最早的實(shí)現(xiàn)相比,這里有幾點(diǎn)可以注意一下:
- require了 'delegate'包
- 繼承自
SimpleDelegator
- 方法體里沒有
def initialize
方法 - 直接調(diào)用了
max_by
方法(這個(gè)是Enumerable
里提供的方法)妥箕,忽略了前面的receiver滥酥。
好了,現(xiàn)在所有都像之前一樣可以使用畦幢,同時(shí)我們還有了array里的所有方法坎吻。基本上來說呛讲,我們是對(duì)一個(gè)array使用了一個(gè)裝飾者模式(百度百科)『痰。現(xiàn)在我們調(diào)用recommended_movies.count
就會(huì)返回3了。
現(xiàn)象背后
源碼地址贝搁。建議新開tab頁打開吗氏,一邊看學(xué)有源碼一邊看本文±啄妫可以使用l
來跳轉(zhuǎn)到指定行數(shù)(github自己的功能)弦讽。
在繼承自SimpleDelegator
之后,它的祖先鏈?zhǔn)沁@樣的:
recommended_movies.class.ancestors
=> [RecommendedMovies, SimpleDelegator, Delegator,
#<Module:0x007fed5005fc90>, BasicObject]
上面這祖先鏈跟我們以前認(rèn)識(shí)的不太一樣——[RecommendedMovies, Object, Kernel, BasicObject]
。原因是SimpleDelegator
繼承自另一個(gè)類——Delegator
(line 316)往产。而它是繼承自BasicObject
(line 39)被碗。這就是為什么Object
跟Kernel
不在這條祖先鏈里的原因。這個(gè)特殊的#<Module:0x007fed5005fc90>
是一個(gè)匿名module仿村,在Delegate
類里定義和引用(included)的(line 53)锐朴;它就像是一個(gè)縮減版的Kernel
:Kernel
被復(fù)制一份被放到一個(gè)臨時(shí)變量里(line 40),之后蔼囊,都在這個(gè)變量的類一級(jí)進(jìn)行操作(line 41)焚志,并把一些方法undef_method
掉。在這些變化做完后畏鼓,這個(gè)kernel可以被Delegate
引用了(include)酱酬。以上就解釋了我們看到的這條祖先鏈。
透明的初始化(transparent initialization)
較早前我們有提到云矫,這里忽略了RecommendedMovies
里的initialize
方法膳沽。Ruby在創(chuàng)建一個(gè)新的object時(shí)會(huì)自動(dòng)調(diào)用initialize
方法,因?yàn)槲覀冊(cè)谶@里沒有定義這個(gè)方法让禀,它就會(huì)去祖先鏈里找挑社。SimpleDelegator
這里也沒有實(shí)現(xiàn)這個(gè)方法,但Delegator
有實(shí)現(xiàn)(line 71)巡揍。它期待一個(gè)單獨(dú)的參數(shù)滔灶,obj
,這個(gè)就是我們?cè)趧?chuàng)建RecommendedMovies
實(shí)例時(shí)傳入的參數(shù)吼肥,在我們的例子里就是一個(gè)Movie
的Array
對(duì)象——也就是我們想要把消息代理過去的對(duì)象录平。
在內(nèi)部,Delegator#initialize
這個(gè)方法就是簡單地調(diào)用了_setobj_
方法缀皱,傳遞同樣的這個(gè)obj
參數(shù)斗这。但Delegator
沒有實(shí)現(xiàn)_setobj_
:如果直接調(diào)用它,會(huì)拋出一個(gè)異常(line 176)啤斗。這是因?yàn)?code>Delegate扮演一個(gè)抽象類的角色表箭。它的子孫類要去實(shí)現(xiàn)_setobj_
方法,實(shí)際上SimpleDelegator
也做了實(shí)現(xiàn)(line 340)钮莲。SimpleDelegator#__setobj__
就是簡單地把obj
存在了一個(gè)名為delegate_sd_obj
的實(shí)例變量里(sd意思為SimpleDelegator)免钻。在我們的例子里,self
仍然是recommended_movies
代理崔拥!
就像之前的示例极舔,一旦我們的recommended_movies
對(duì)象產(chǎn)生,我們就可以用它來裝飾一個(gè)array链瓦。我們可以在它上面調(diào)用best
方法拆魏,Ruby可以定位到這個(gè)對(duì)象的class盯桦,RecommendedMovies
,并為我們執(zhí)行它渤刃。但當(dāng)我們調(diào)用count
時(shí)拥峦,Ruby找不到對(duì)應(yīng)的方法。之后去它的祖先鏈里去找卖子,但也沒有count
方法略号。
這時(shí)就需要定義method_missing
方法。如果Ruby在通常的方法查找過程中沒有找到方法洋闽,它不會(huì)立即拋出NoMethodError
方法璃哟;相反,它會(huì)繼續(xù)查找喊递,這次會(huì)去method_missing
里找。如果任何一個(gè)祖先類里定義了這個(gè)方法阳似,就會(huì)被調(diào)用到骚勘。如果沒有的話,我們會(huì)收到NoMethodError
錯(cuò)誤撮奏。
在我們的上下文里俏讹,Delegator
類定義了method_missing
方法(line 78)。首先畜吊,它通過調(diào)用_getobj_
方法泽疆,得到了我們想要代理過去的目標(biāo)對(duì)象(line 80),是在(line 318)里實(shí)現(xiàn)的玲献。實(shí)際上殉疼,這個(gè)方法是把我們存在@delegate_sd_obj
里的對(duì)象拿了出來。之后用question方法試一下這個(gè)對(duì)象能否調(diào)用這個(gè)方法(line 83)捌年。如果不行的話瓢娜,Delegate#method_missing
會(huì)檢查是否Kernel
可以調(diào)用這個(gè)方法,如果可以礼预,就去調(diào)用(line 85)眠砾,否則的話就會(huì)調(diào)用super
(line 87),在這里托酸,得到的結(jié)果就是NoMethodError
褒颈。
在method_missing
里還有些其他的代碼,不過剛才說過的就是這里的核心部分励堡。在《Ruby元編程》里有提到過“Blank Slate”谷丸,就是一個(gè)只有最小數(shù)量方法的類。Delegate
類就是用到這個(gè)技術(shù)应结,它繼承自BasicObject
淤井,消除了不必要意外,但同時(shí)也要注意到method_missing
的實(shí)現(xiàn),里面詢問這個(gè)目標(biāo)對(duì)象是否能repond一個(gè)特定的方法币狠,這個(gè)目標(biāo)對(duì)象一般會(huì)是繼承自Object
游两。這個(gè)內(nèi)部原理有些復(fù)雜,但到最后漩绵,我們得到了一個(gè)比較簡單且直觀的接口(RecommendedMovies
類)贱案。沒準(zhǔn)在你的代碼里也可以用到代理這個(gè)技術(shù)來進(jìn)行一些重構(gòu)。