設(shè)計(jì)模式學(xué)習(xí)(二十四):訪問者模式

一嗅钻、前言

設(shè)計(jì)模式的基礎(chǔ)學(xué)習(xí)進(jìn)入了尾聲挨务,本周在家看了一下《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》中介紹的最后一個(gè)設(shè)計(jì)模式——訪問者模式,在此寫篇博客記錄一下千诬。

雖然通過閱讀《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》以及與同事開展研討會(huì)學(xué)習(xí)了各種設(shè)計(jì)模式娱据,但這還遠(yuǎn)遠(yuǎn)不夠臣淤,后續(xù)需要在實(shí)踐中應(yīng)用這些設(shè)計(jì)模式贰健,寫出易擴(kuò)展、易用惶凝、易維護(hù)的代碼束莫,提高自己的開發(fā)能力剩岳。

二坎缭、訪問者模式

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

意圖:主要將數(shù)據(jù)結(jié)構(gòu)與數(shù)據(jù)操作分離郊愧,訪問者模式能將算法與其所作用的對(duì)象隔離開來澡腾。

比如沸伏,有一位非常希望贏得新客戶的資深保險(xiǎn)代理人。 他可以拜訪街區(qū)中的每棟樓动分, 嘗試向每個(gè)路人推銷保險(xiǎn)。 所以根據(jù)大樓內(nèi)組織類型的不同红选, 他可以提供專門的保單:

  • 如果建筑是居民樓澜公, 他會(huì)推銷醫(yī)療保險(xiǎn)。
  • 如果建筑是銀行喇肋, 他會(huì)推銷失竊保險(xiǎn)坟乾。
  • 如果建筑是咖啡廳, 他會(huì)推銷火災(zāi)和洪水保險(xiǎn)蝶防。

三甚侣、示例介紹

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

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

在這里插入圖片描述

上面的框圖顯示了 Node 類層次的一部分轮纫。這里的問題是,將所有這些操作分散到各種結(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)中搓谆,并在遍歷抽象語法樹時(shí)將此對(duì)象傳遞給當(dāng)前訪問的元素炒辉。當(dāng)一個(gè)元素“接受”該訪問者時(shí),該元素向訪問者發(fā)送一個(gè)包含自身類信息的請(qǐng)求泉手。該請(qǐng)求同時(shí)也將該元素本身作為一個(gè)參數(shù)黔寇。然后訪問者將為該元素執(zhí)行該操作—這一操作以前是在該元素的類中的。

例如斩萌,一個(gè)不使用訪問者的編譯器可能會(huì)通過在它的抽象語法樹上調(diào)用 TypeCheck 操作對(duì)一個(gè)過程進(jìn)行類型檢查缝裤。每一個(gè)結(jié)點(diǎn)將對(duì)調(diào)用它的成員的 TypeCheck 以實(shí)現(xiàn)自身的 TypeCheck 。

如果該編譯器使用訪問者對(duì)一個(gè)過程進(jìn)行類型檢查术裸,那么它將會(huì)創(chuàng)建一個(gè)TypeCheckingVisitor 對(duì)象倘是,并以這個(gè)對(duì)象為一個(gè)參數(shù)在抽象語法樹上調(diào)用 Accept 操作。每一個(gè)結(jié)點(diǎn)在實(shí)現(xiàn) Accept 時(shí)將會(huì)回調(diào)訪問者:一個(gè)賦值結(jié)點(diǎn)調(diào)用訪問者的 VisitAssignment 操作袭艺,而一個(gè)變量引用將調(diào)用 VisitVariableReference搀崭。以前類 AssignmentNode 的 TypeCheck 操作現(xiàn)在成為 TypeCheckingVisitor 的 VisitAssignment 操作。

為使訪問者不僅僅只做類型檢查猾编,我們需要所有抽象語法樹的訪問者有一個(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 中。

在這里插入圖片描述

在這里插入圖片描述

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

四、結(jié)構(gòu)與參與者

在這里插入圖片描述

Visitor:(訪問者渤涌,如NodeVisitor)

為該對(duì)象結(jié)構(gòu)中 ConcreteElement 的每一個(gè)類聲明一個(gè) Visit 操作佩谣。該操作的名字和特征標(biāo)識(shí)了發(fā)送 Visit 請(qǐng)求給該訪問者的那個(gè)類。這使得訪問者可以確定正被訪問元素的具體的類实蓬。這樣訪問者就可以通過該元素的特定接口直接訪問它茸俭。

ConcreteVisitor:(具體訪問者吊履,如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)的過程中累積結(jié)果腾窝。

Element:(元素冕臭,如Node)

定義一個(gè) Accept 操作,它以一個(gè)訪問者為參數(shù)燕锥。

ConcreteElement:(具體元素,如 AssignmentNode悯蝉,VariableRefNode)

實(shí)現(xiàn) Accept 操作归形,該操作以一個(gè)訪問者為參數(shù)。

ObjectStructure:(對(duì)象結(jié)構(gòu)鼻由,如 Program)

  1. 能枚舉它的元素暇榴。
  2. 可以提供一個(gè)高層的接口以允許該訪問者訪問它的元素。
  3. 可以是一個(gè)復(fù)合或是一個(gè)集合蕉世,如一個(gè)列表或一個(gè)無序集合蔼紧。

五、協(xié)作

  • 一個(gè)使用 Visitor 模式的客戶必須創(chuàng)建一個(gè) ConcreteVisitor 對(duì)象狠轻,然后遍歷該對(duì)象結(jié)構(gòu)奸例,并用該訪問者訪問每一個(gè)元素。
  • 當(dāng)一個(gè)元素被訪問時(shí)向楼,它調(diào)用對(duì)應(yīng)于它的類的 Visitor 操作查吊。如果必要,該元素將自身作為這個(gè)操作的一個(gè)參數(shù)以便該訪問者訪問它的狀態(tài)湖蜕。

下面的交互框圖說明了一個(gè)對(duì)象結(jié)構(gòu)逻卖、一個(gè)訪問者和兩個(gè)元素之間的協(xié)作。

在這里插入圖片描述

六昭抒、簡(jiǎn)單的示例代碼

// main.cc
class ConcreteComponentA;
class ConcreteComponentB;

class Visitor {
 public:
  virtual void VisitConcreteComponentA(const ConcreteComponentA *element) const = 0;
  virtual void VisitConcreteComponentB(const ConcreteComponentB *element) const = 0;
};

class Component {
 public:
  virtual ~Component() {}
  virtual void Accept(Visitor *visitor) const = 0;
};

class ConcreteComponentA : public Component {
 public:
  void Accept(Visitor *visitor) const override {
    visitor->VisitConcreteComponentA(this);
  }
  std::string ExclusiveMethodOfConcreteComponentA() const {
    return "A";
  }
};

class ConcreteComponentB : public Component {
 public:
  void Accept(Visitor *visitor) const override {
    visitor->VisitConcreteComponentB(this);
  }
  std::string SpecialMethodOfConcreteComponentB() const {
    return "B";
  }
};

class ConcreteVisitor1 : public Visitor {
 public:
  void VisitConcreteComponentA(const ConcreteComponentA *element) const override {
    std::cout << element->ExclusiveMethodOfConcreteComponentA() << " + ConcreteVisitor1\n";
  }

  void VisitConcreteComponentB(const ConcreteComponentB *element) const override {
    std::cout << element->SpecialMethodOfConcreteComponentB() << " + ConcreteVisitor1\n";
  }
};

class ConcreteVisitor2 : public Visitor {
 public:
  void VisitConcreteComponentA(const ConcreteComponentA *element) const override {
    std::cout << element->ExclusiveMethodOfConcreteComponentA() << " + ConcreteVisitor2\n";
  }
  void VisitConcreteComponentB(const ConcreteComponentB *element) const override {
    std::cout << element->SpecialMethodOfConcreteComponentB() << " + ConcreteVisitor2\n";
  }
};

void ClientCode(std::array<const Component *, 2> components, Visitor *visitor) {
  // ...
  for (const Component *comp : components) {
    comp->Accept(visitor);
  }
  // ...
}

int main() {
  std::array<const Component *, 2> components = {new ConcreteComponentA, new ConcreteComponentB};
  std::cout << "客戶端代碼通過訪問者基類接口調(diào)用所有訪問者實(shí)體類:\n";
  ConcreteVisitor1 *visitor1 = new ConcreteVisitor1;
  ClientCode(components, visitor1);
  std::cout << "\n";
  std::cout << "它允許相同的客戶端代碼與不同類型的訪問者一起工作:\n";
  ConcreteVisitor2 *visitor2 = new ConcreteVisitor2;
  ClientCode(components, visitor2);

  for (const Component *comp : components) {
    delete comp;
  }
  delete visitor1;
  delete visitor2;

  return 0;
}

輸出結(jié)果:

客戶端代碼通過訪問者基類接口調(diào)用所有訪問者實(shí)體類:
A + ConcreteVisitor1
B + ConcreteVisitor1

它允許相同的客戶端代碼與不同類型的訪問者一起工作:
A + ConcreteVisitor2
B + ConcreteVisitor2

七评也、總結(jié)

7.1 優(yōu)缺點(diǎn)

優(yōu)點(diǎn):符合單一職責(zé)原則、優(yōu)秀的擴(kuò)展性灭返、靈活性盗迟。

  1. 訪問者模式使得易于增加新的操作:訪問者使得增加依賴于復(fù)雜對(duì)象結(jié)構(gòu)的構(gòu)件的操作變得容易了。僅需增加一個(gè)新的訪問者即可在一個(gè)對(duì)象結(jié)構(gòu)上定義一個(gè)新的操作婆殿。相反诈乒,如果每個(gè)功能都分散在多個(gè)類之上的話,定義新的操作時(shí)必須修改每一類婆芦。
  2. 訪問者集中相關(guān)的操作而分離無關(guān)的操作:相關(guān)的行為不是分布在定義該對(duì)象結(jié)構(gòu)的各個(gè)類上怕磨,而是集中在一個(gè)訪問者中喂饥。無關(guān)行為卻被分別放在它們各自的訪問者子類中。這就既簡(jiǎn)化了這些元素的類肠鲫,也簡(jiǎn)化了在這些訪問者中定義的算法员帮。所有與它的算法相關(guān)的數(shù)據(jù)結(jié)構(gòu)都可以被隱藏在訪問者中。

缺點(diǎn):具體元素對(duì)訪問者公布細(xì)節(jié)导饲,違反了迪米特原則捞高、具體元素變更比較困難、違反了依賴倒置原則渣锦。

  1. 增加新的 ConcreteElement 類很困難:每添加一個(gè)新的 ConcreteElement 都要在 Vistor 中添加一個(gè)新的抽象操作硝岗,并在每一個(gè) ConcretVisitor 類中實(shí)現(xiàn)相應(yīng)的操作。有時(shí)可以在 Visitor 中提供一個(gè)缺省的實(shí)現(xiàn)袋毙,這一實(shí)現(xiàn)可以被大多數(shù)的ConcreteVisitor 繼承型檀,但這與其說是一個(gè)規(guī)律還不如說是一種例外。
  2. 破壞封裝:訪問者方法假定 ConcreteElement 接口的功能足夠強(qiáng)听盖,足以讓訪問者進(jìn)行它們的工作胀溺。結(jié)果是,該模式常常迫使你提供訪問元素內(nèi)部狀態(tài)的公共操作皆看,這可能會(huì)破壞它的封裝性仓坞。

7.2 應(yīng)用場(chǎng)景

在下列情況下使用 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)的操作集中起來定義在一個(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ì)所有訪問者的接口始藕,這可能需要很大的代價(jià)蒲稳。如果對(duì)象結(jié)構(gòu)類經(jīng)常改變,那么可能還是在這些類中定義這些操作較好伍派。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末江耀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子诉植,更是在濱河造成了極大的恐慌祥国,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異舌稀,居然都是意外死亡啊犬,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門壁查,熙熙樓的掌柜王于貴愁眉苦臉地迎上來觉至,“玉大人,你說我怎么就攤上這事睡腿∮镉” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵席怪,是天一觀的道長(zhǎng)应闯。 經(jīng)常有香客問我,道長(zhǎng)挂捻,這世上最難降的妖魔是什么孽锥? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮细层,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘唬涧。我一直安慰自己疫赎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布碎节。 她就那樣靜靜地躺著捧搞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪狮荔。 梳的紋絲不亂的頭發(fā)上胎撇,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音殖氏,去河邊找鬼晚树。 笑死,一個(gè)胖子當(dāng)著我的面吹牛雅采,可吹牛的內(nèi)容都是我干的爵憎。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼婚瓜,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼宝鼓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起巴刻,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤愚铡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后胡陪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沥寥,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碍舍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了营曼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乒验。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蒂阱,靈堂內(nèi)的尸體忽然破棺而出锻全,到底是詐尸還是另有隱情,我是刑警寧澤录煤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布鳄厌,位于F島的核電站,受9級(jí)特大地震影響妈踊,放射性物質(zhì)發(fā)生泄漏了嚎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一廊营、第九天 我趴在偏房一處隱蔽的房頂上張望歪泳。 院中可真熱鬧,春花似錦露筒、人聲如沸呐伞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽伶氢。三九已至,卻和暖如春瘪吏,著一層夾襖步出監(jiān)牢的瞬間癣防,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工掌眠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蕾盯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓蓝丙,卻偏偏與公主長(zhǎng)得像刑枝,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子迅腔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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