5.11 VISITOR(訪問(wèn)者) — 對(duì)象行為型模式

1 意圖

表示一個(gè)作用于某對(duì)象結(jié)構(gòu)中的各元素操作。它使你可以在不改變各元素的類的前提下定義作用于這些元素的新操作。

2 動(dòng)機(jī)

考慮一個(gè)編譯器,它將源程序表示為一個(gè)抽象語(yǔ)法樹。該編譯器需在抽象語(yǔ)法樹上實(shí)施某些操作以進(jìn)行“靜態(tài)語(yǔ)義”分析,例如檢查是否所有的變量都已經(jīng)被定義了。它也需要生成代碼。因此它可能要定義許多操作以進(jìn)行類型檢查、代碼優(yōu)化、流程分析,檢查變量是否在使用前被賦初值,等等。此外,還可使用抽象語(yǔ)法樹進(jìn)行優(yōu)美格式打印、程序重構(gòu)、code,instrumentation以及對(duì)程序進(jìn)行多種度量。

這些操作大多要求對(duì)不同的節(jié)點(diǎn)進(jìn)行不同的處理。例如對(duì)代表賦值語(yǔ)句的結(jié)點(diǎn)的處理就不同于對(duì)代表變量或算術(shù)表達(dá)式的結(jié)點(diǎn)的處理。因此有用于賦值語(yǔ)句的類,有用于變量訪問(wèn)的類,還有用于算術(shù)表達(dá)式的類,等等。結(jié)點(diǎn)類的集合當(dāng)然依賴于被編譯的語(yǔ)言,但對(duì)于一個(gè)給定的語(yǔ)言其變化不大。


image.png

上面的框圖顯示了Node類層次的一部分。這里的問(wèn)題是,將所有這些操作分散到各種結(jié)點(diǎn)類中會(huì)導(dǎo)致整個(gè)系統(tǒng)難以理解、難以維護(hù)和修改。將類型檢查代碼與優(yōu)美格式打印代碼或流程分析代碼放在一起,將產(chǎn)生混亂。此外,增加新的操作通常需要重新編譯所有這些類。如果可以獨(dú)立地增加新的操作,并且使這些結(jié)點(diǎn)類獨(dú)立于作用于其上的操作,將會(huì)更好一些。

要實(shí)現(xiàn)上述兩個(gè)目標(biāo),我們可以將每一個(gè)類中相關(guān)的操作包裝在一個(gè)獨(dú)立的對(duì)象(稱為一個(gè)Visitor)中,并在遍歷抽象語(yǔ)法樹時(shí)將此對(duì)象傳遞給當(dāng)前訪問(wèn)的元素。當(dāng)一個(gè)元素“接受”該訪問(wèn)者時(shí),該元素向訪問(wèn)者發(fā)送一個(gè)包含自身類信息的請(qǐng)求。該請(qǐng)求同時(shí)也將該元素本身作為一個(gè)參數(shù)。然后訪問(wèn)者將為該元素執(zhí)行該操作 — 這一操作以前是在該元素的類中的。

例如,一個(gè)不使用訪問(wèn)者的編譯器可能會(huì)通過(guò)在它的抽象語(yǔ)法樹上調(diào)用 TypeCheck操作對(duì)一個(gè)過(guò)程進(jìn)行類型檢查。每一個(gè)結(jié)點(diǎn)將對(duì)調(diào)用它的成員的TypeCheck以實(shí)現(xiàn)自身的TypeCheck(參見前面的類框圖)。如果該編譯器使用訪問(wèn)者對(duì)一個(gè)過(guò)程進(jìn)行類型檢查,那么它將會(huì)創(chuàng)建一個(gè)TypeCheckingVisitor對(duì)象,并以這個(gè)對(duì)象為一個(gè)參數(shù)在抽象語(yǔ)法樹上調(diào)用 Accept操作。每個(gè)結(jié)點(diǎn)在實(shí)現(xiàn)Accept時(shí)將會(huì)回調(diào)訪問(wèn)者:一個(gè)賦值結(jié)點(diǎn)調(diào)用訪問(wèn)者的VisitAssignment操作,而一個(gè)變量引用將調(diào)用VisitVariableReference。以前類AssignmentNode的TypeCheck操作現(xiàn)在成為TypeCheckingVisitor的VisitAssignment操作。

為使訪問(wèn)者不僅僅只做類型檢查,我們需要所有抽象語(yǔ)法樹的訪問(wèn)者有一個(gè)抽象的父類NodeVisitor。NodeVisitor必須為每一個(gè)結(jié)點(diǎn)類定義一個(gè)操作。一個(gè)需要計(jì)算程序度量的應(yīng)用將定義NodeVisitor的新的子類,并且將不再需要在結(jié)點(diǎn)類中增加與特定應(yīng)用相關(guān)的代碼。Visitor模式將每一個(gè)編譯步驟的操作封裝在一個(gè)與該步驟相關(guān)的Visitor中(參加下圖)。


image.png

使用Visitor模式,必須定義兩個(gè)類層次:一個(gè)對(duì)應(yīng)于接受操作的元素( Node層次)另一個(gè)對(duì)應(yīng)于定義對(duì)元素的操作的訪問(wèn)者( NodeVisitor層次)。給訪問(wèn)者類層次增加一個(gè)新的子類即可創(chuàng)建一個(gè)新的操作。只要該編譯器接受的語(yǔ)法不改變(即不需要增加新的 Node子類),我們就可以簡(jiǎn)單的定義新的NodeVisitor子類以增加新的功能。

3 適用性

在下列情況下使用Visitor模式:

  • 一個(gè)對(duì)象結(jié)構(gòu)包含很多類對(duì)象,它們有不同的接口,而你想對(duì)這些對(duì)象實(shí)施一些依賴于其具體類的操作。
  • 需要對(duì)一個(gè)對(duì)象結(jié)構(gòu)中的對(duì)象進(jìn)行很多不同的并且不相關(guān)的操作,而你想避免讓這些操作“污染”這些對(duì)象的類。 Visitor使得你可以將相關(guān)的操作集中起來(lái)定義在一個(gè)類中。當(dāng)該對(duì)象結(jié)構(gòu)被很多應(yīng)用共享時(shí),用Visitor模式讓每個(gè)應(yīng)用僅包含需要用到的操作。
  • 定義對(duì)象結(jié)構(gòu)的類很少改變,但經(jīng)常需要在此結(jié)構(gòu)上定義新的操作。改變對(duì)象結(jié)構(gòu)類需要重定義對(duì)所有訪問(wèn)者的接口,這可能需要很大的代價(jià)。如果對(duì)象結(jié)構(gòu)類經(jīng)常改變,那么可能還是在這些類中定義這些操作較好。
4 結(jié)構(gòu)
image.png
5 參與者
  • Visitor(訪問(wèn)者,如NodeVisitor)
    ——為該對(duì)象結(jié)構(gòu)中ConcreteElement的每一個(gè)類聲明一個(gè)Visit操作。該操作的名字和特征標(biāo)識(shí)了發(fā)送Visit請(qǐng)求給該訪問(wèn)者的那個(gè)類。這使得訪問(wèn)者可以確定正被訪問(wèn)元素的具體的類。這樣訪問(wèn)者就可以通過(guò)該元素的特定接口直接訪問(wèn)它。
  • ConcreteVisitor(具體訪問(wèn)者,如TypeCheckingVisitor)
    —— 實(shí)現(xiàn)每個(gè)由Visitor聲明的操作。每個(gè)操作實(shí)現(xiàn)本算法的一部分,而該算法片斷乃是對(duì)應(yīng)于結(jié)構(gòu)中對(duì)象的類。ConcreteVisitor為該算法提供了上下文并存儲(chǔ)它的局部狀態(tài)。這一狀態(tài)常常在遍歷該結(jié)構(gòu)的過(guò)程中累積結(jié)果。
  • Element(元素,如Node)
    ——定義一個(gè)Accept操作,它以一個(gè)訪問(wèn)者為參數(shù)。
    -ConcreteElement(具體元素,如AssignmentNode,VariableRefNode)
    ——實(shí)現(xiàn)Accept操作,該操作以一個(gè)訪問(wèn)者為參數(shù)。
  • ObjectStructure(對(duì)象結(jié)構(gòu),如Program)
    ——能枚舉它的元素。
    ——可以提供一個(gè)高層的接口以允許該訪問(wèn)者訪問(wèn)它的元素。
    ——可以是一個(gè)復(fù)合(參見Composite(4 . 3))或是一個(gè)集合,如一個(gè)列表或一個(gè)無(wú)序集合。
6 協(xié)作
  • 一個(gè)使用Visitor模式的客戶必須創(chuàng)建一個(gè)ConcreteVisitor對(duì)象,然后遍歷該對(duì)象結(jié)構(gòu),并用該訪問(wèn)者訪問(wèn)每一個(gè)元素。
  • 當(dāng)一個(gè)元素被訪問(wèn)時(shí),它調(diào)用對(duì)應(yīng)于它的類的Visitor操作。如果必要,該元素將自身作為這個(gè)操作的一個(gè)參數(shù)以便該訪問(wèn)者訪問(wèn)它的狀態(tài)。
    下面的交互框圖說(shuō)明了一個(gè)對(duì)象結(jié)構(gòu)、一個(gè)訪問(wèn)者和兩個(gè)元素之間的協(xié)作。


    image.png
7 效果
  • 1 訪問(wèn)者模式使得易于增加新的操作:訪問(wèn)者使得增加依賴于復(fù)雜對(duì)象結(jié)構(gòu)的構(gòu)件的操作變得容易了。僅需增加一個(gè)新的訪問(wèn)者即可在一個(gè)對(duì)象結(jié)構(gòu)上定義一個(gè)新的操作。相反,如果每個(gè)功能都分散在多個(gè)類之上的話,定義新的操作時(shí)必須修改每一類。
  • 2 訪問(wèn)者集中相關(guān)的操作而分離無(wú)關(guān)的操作:相關(guān)的行為不是分布在定義該對(duì)象結(jié)構(gòu)的各個(gè)類上,而是集中在一個(gè)訪問(wèn)者中。無(wú)關(guān)行為卻被分別放在它們各自的訪問(wèn)者子類中。這就既簡(jiǎn)化了這些元素的類,也簡(jiǎn)化了在這些訪問(wèn)者中定義的算法。所有與它的算法相關(guān)的數(shù)據(jù)結(jié)構(gòu)都可以被隱藏在訪問(wèn)者中。
  • 3 增加新的ConcreteElement類很困難Visitor模式使得難以增加新的Element的子類。每添加一個(gè)新的ConcreteElement都要在Visitor中添加一個(gè)新的抽象操作,并在每一個(gè)ConcreteVisitor類中實(shí)現(xiàn)相應(yīng)的操作。有時(shí)可以在Visitor中提供一個(gè)缺省的實(shí)現(xiàn),這一實(shí)現(xiàn)可以被大多數(shù)的ConcreteVisitor繼承,但這與其說(shuō)是一個(gè)規(guī)律還不如說(shuō)是一種例外。
    所以在應(yīng)用訪問(wèn)者模式時(shí)考慮關(guān)鍵的問(wèn)題是系統(tǒng)的哪個(gè)部分會(huì)經(jīng)常變化,是作用于對(duì)象結(jié)構(gòu)上的算法呢還是構(gòu)成該結(jié)構(gòu)的各個(gè)對(duì)象的類。如果老是有新的ConcreteElement類加入進(jìn)來(lái)的話,Visitor類層次將變得難以維護(hù)。在這種情況下,直接在構(gòu)成該結(jié)構(gòu)的類中定義這些操作可能更容易一些。如果Element類層次是穩(wěn)定的,而你不斷地增加操作獲修改算法,訪問(wèn)者模式可以幫助你管理這些改動(dòng)。
  • 4 通過(guò)類層次進(jìn)行訪問(wèn):一個(gè)迭代器(參見Iterator(5 . 4))可以通過(guò)調(diào)用節(jié)點(diǎn)對(duì)象的特定操作來(lái)遍歷整個(gè)對(duì)象結(jié)構(gòu),同時(shí)訪問(wèn)這些對(duì)象。但是迭代器不能對(duì)具有不同元素類型的對(duì)象結(jié)構(gòu)進(jìn)行操作。
  • 5 累積狀態(tài):當(dāng)訪問(wèn)者訪問(wèn)對(duì)象結(jié)構(gòu)中的每一個(gè)元素時(shí),它可能會(huì)累積狀態(tài)。如果沒(méi)有訪問(wèn)者,這一狀態(tài)將作為額外的參數(shù)傳遞給進(jìn)行遍歷的操作,或者定義為全局變量。
  • 6 破壞封裝: 訪問(wèn)者方法假定ConcreteElement接口的功能足夠強(qiáng),足以讓訪問(wèn)者進(jìn)行它們的工作。結(jié)果是,該模式常常迫使你提供訪問(wèn)元素內(nèi)部狀態(tài)的公共操作,這可能會(huì)破壞它的封裝性。
8 實(shí)現(xiàn)

每一個(gè)對(duì)象結(jié)構(gòu)將有一個(gè)相關(guān)的Vi s i t o r類。這個(gè)抽象的訪問(wèn)者類為定義對(duì)象結(jié)構(gòu)的每一個(gè)ConcreteElement類聲明一個(gè)VisitConcreteElement操作。每一個(gè)Visitor上的Visit操作聲明它的參數(shù)為一個(gè)特定的ConcreteElement,以允許該Visitor直接訪問(wèn)ConcreteElement的接口。ConcreteVisitor類重定義每一個(gè)Visit操作,從而為相應(yīng)的ConcreteElement類實(shí)現(xiàn)與特定訪問(wèn)者相關(guān)的行為。
下面是當(dāng)應(yīng)用Vi s i t o r模式時(shí)產(chǎn)生的其他兩個(gè)實(shí)現(xiàn)問(wèn)題:

  • 1 雙分派:
  • 2 誰(shuí)負(fù)責(zé)遍歷對(duì)象結(jié)構(gòu)
9 代碼示例

github地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,416評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,170評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,597評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評(píng)論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,029評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,403評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,746評(píng)論 2 370

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