從源碼上來看ruby標(biāo)準(zhǔn)庫里的delegate.rb

譯自這篇文章苇倡。這篇文章里涉及到了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è)電影推薦的后端。簡化的來看返咱,我們的Moviescore的值是從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)可以注意一下:

  1. require了 'delegate'包
  2. 繼承自SimpleDelegator
  3. 方法體里沒有def initialize方法
  4. 直接調(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)被碗。這就是為什么ObjectKernel不在這條祖先鏈里的原因。這個(gè)特殊的#<Module:0x007fed5005fc90>是一個(gè)匿名module仿村,在Delegate類里定義和引用(included)的(line 53)锐朴;它就像是一個(gè)縮減版的KernelKernel被復(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è)MovieArray對(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)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末止吐,一起剝皮案震驚了整個(gè)濱河市宝踪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌碍扔,老刑警劉巖瘩燥,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異不同,居然都是意外死亡厉膀,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門二拐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來服鹅,“玉大人,你說我怎么就攤上這事百新∑笕恚” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵饭望,是天一觀的道長仗哨。 經(jīng)常有香客問我,道長铅辞,這世上最難降的妖魔是什么藻治? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮巷挥,結(jié)果婚禮上桩卵,老公的妹妹穿的比我還像新娘。我一直安慰自己倍宾,他們只是感情好雏节,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著高职,像睡著了一般钩乍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上怔锌,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天寥粹,我揣著相機(jī)與錄音变过,去河邊找鬼。 笑死涝涤,一個(gè)胖子當(dāng)著我的面吹牛媚狰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播阔拳,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼崭孤,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了糊肠?” 一聲冷哼從身側(cè)響起辨宠,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎货裹,沒想到半個(gè)月后嗤形,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弧圆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年赋兵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墓阀。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖拓轻,靈堂內(nèi)的尸體忽然破棺而出斯撮,到底是詐尸還是另有隱情,我是刑警寧澤扶叉,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布勿锅,位于F島的核電站,受9級(jí)特大地震影響枣氧,放射性物質(zhì)發(fā)生泄漏溢十。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一达吞、第九天 我趴在偏房一處隱蔽的房頂上張望张弛。 院中可真熱鬧,春花似錦酪劫、人聲如沸吞鸭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽刻剥。三九已至,卻和暖如春滩字,著一層夾襖步出監(jiān)牢的瞬間造虏,已是汗流浹背御吞。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留漓藕,地道東北人陶珠。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像撵术,于是被迫代替她去往敵國和親背率。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容