一起詳細(xì)聊聊C#中的Visitor模式
寫(xiě)在前面
Visitor模式在日常工作中出場(chǎng)比較少,如果統(tǒng)計(jì)大家不熟悉的模式,那么它榜上有名的可能性非常大。使用頻率少,再加上很多文章提到Visitor模式都著重于它克服語(yǔ)言單分派的特點(diǎn)上面,而對(duì)何時(shí)應(yīng)該使用這個(gè)模式及這個(gè)模式是怎么一點(diǎn)點(diǎn)演講出來(lái)的提之甚少,造成很多人對(duì)這個(gè)模式有種霧里看花的感覺(jué),今天跟著老胡,我們一起來(lái)一點(diǎn)點(diǎn)揭開(kāi)它的面紗吧。
模式演進(jìn)
舉個(gè)例子
現(xiàn)在假設(shè)我們有一個(gè)簡(jiǎn)單的需求,需要統(tǒng)計(jì)出一篇文檔中的字?jǐn)?shù)、詞數(shù)和圖片數(shù)量。其中字?jǐn)?shù)和詞數(shù)存在于段落中,圖片數(shù)量單獨(dú)統(tǒng)計(jì)。于是乎,我們可以很快的寫(xiě)出第一版代碼
使用了基本抽象的版本
abstract class DocumentElement { public abstract void UpdateStatus(DocumentStatus status); } public class DocumentStatus { public int CharNum { get; set; } public int WordNum { get; set; } public int ImageNum { get; set; } public void ShowStatus() { Console.WriteLine("I have {0} char, {1} word and {2} image", CharNum, WordNum, ImageNum); } } class ImageElement : DocumentElement { public override void UpdateStatus(DocumentStatus status) { status.ImageNum++; } } class ParagraphElement : DocumentElement { public int CharNum { get; set; } public int WordNum { get; set; } public ParagraphElement(int charNum, int wordNum) { CharNum = charNum; WordNum = wordNum; } public override void UpdateStatus(DocumentStatus status) { status.CharNum += CharNum; status.WordNum += WordNum; } } class Program { static void Main(string[] args) { DocumentStatus docStatus = new DocumentStatus(); List<DocumentElement> list = new List<DocumentElement>(); DocumentElement e1 = new ImageElement(); DocumentElement e2 = new ParagraphElement(10, 20); list.Add(e1); list.Add(e2); list.ForEach(e => e.UpdateStatus(docStatus)); docStatus.ShowStatus(); } }
運(yùn)行結(jié)果如下,非常簡(jiǎn)單
但是細(xì)看這版代碼,會(huì)發(fā)現(xiàn)有以下問(wèn)題:
- 所有的DocumentElement派生類(lèi)必須訪問(wèn)DocumentStatus,根據(jù)迪米特法則,這不是個(gè)好現(xiàn)象,如果在未來(lái)對(duì)DocumentStatus有修改,這些派生類(lèi)被波及的可能性極大
- 統(tǒng)計(jì)代碼散落在不同的派生類(lèi)里面,維護(hù)不方便
有鑒于此,我們推出了第二版代碼
使用了Tpye-Switch的版本
這一版代碼中,我們摒棄了之前在具體的DocumentElement派生類(lèi)中進(jìn)行統(tǒng)計(jì)的做法,直接在統(tǒng)計(jì)類(lèi)中統(tǒng)一處理
public abstract class DocumentElement { //nothing to do now } public class DocumentStatus { public int CharNum { get; set; } public int WordNum { get; set; } public int ImageNum { get; set; } public void ShowStatus() { Console.WriteLine("I have {0} char, {1} word and {2} image", CharNum, WordNum, ImageNum); } public void Update(DocumentElement documentElement) { switch(documentElement) { case ImageElement imageElement: ImageNum++; break; case ParagraphElement paragraphElement: WordNum += paragraphElement.WordNum; CharNum += paragraphElement.CharNum; break; } } } public class ImageElement : DocumentElement { } public class ParagraphElement : DocumentElement { public int CharNum { get; set; } public int WordNum { get; set; } public ParagraphElement(int charNum, int wordNum) { CharNum = charNum; WordNum = wordNum; } } class Program { static void Main(string[] args) { DocumentStatus docStatus = new DocumentStatus(); List<DocumentElement> list = new List<DocumentElement>(); DocumentElement e1 = new ImageElement(); DocumentElement e2 = new ParagraphElement(10, 20); list.Add(e1); list.Add(e2); docStatus.ShowStatus(); } }
測(cè)試結(jié)果和第一個(gè)版本的代碼一樣,這一版代碼克服了第一個(gè)版本中,統(tǒng)計(jì)代碼散落,具體類(lèi)依賴(lài)統(tǒng)計(jì)類(lèi)的問(wèn)題,轉(zhuǎn)而我們?cè)诮y(tǒng)計(jì)類(lèi)中集中處理了統(tǒng)計(jì)任務(wù)。但同時(shí)它引入了type-switch, 這也是一個(gè)不好的信號(hào),具體表現(xiàn)在:
- 代碼冗長(zhǎng)且難以維護(hù)
- 如果派生層次加多,需要很小心的選擇case順序以防出現(xiàn)繼承層次較低的類(lèi)出現(xiàn)在繼承層次更遠(yuǎn)的類(lèi)前面,從而造成后面的case永遠(yuǎn)無(wú)法被訪問(wèn)的情況,這造成了額外的精力成本
嘗試使用重載的版本
有鑒于上面type-switch版本的問(wèn)題,作為敏銳的程序員,可能馬上有人就會(huì)提出重載方案:“如果我們針對(duì)每個(gè)具體的DocumentElement寫(xiě)出相應(yīng)的Update方法,不就可以了嗎?”就像下面這樣
public class DocumentStatus { //省略相同代碼 public void Update(ImageElement imageElement) { ImageNum++; } public void Update(ParagraphElement paragraphElement) { WordNum += paragraphElement.WordNum; CharNum += paragraphElement.CharNum; } } //省略相同代碼 class Program { static void Main(string[] args) { DocumentStatus docStatus = new DocumentStatus(); List<DocumentElement> list = new List<DocumentElement>(); list.Add(new ImageElement()); list.Add(new ParagraphElement(10, 20)); list.ForEach(e => docStatus.Update(e)); docStatus.ShowStatus(); } }
看起來(lái)很好,不過(guò)可惜,這段代碼編譯失敗,編譯器會(huì)抱怨說(shuō),不能將DocumentElement轉(zhuǎn)為它的子類(lèi),這是為什么呢?講到這里,就不能不提一下編程語(yǔ)言中的單分派和雙分派
單分派與雙分派
大家都知道,多態(tài)是OOP的三個(gè)基本特征之一,即形如以下的代碼
public class Father { public virtual void DoSomething(string str){} } public class Son : Father { public override void DoSomething(string str){} } Father son = new Son(); son.DoSomething();
son 雖然被聲明為Father類(lèi)型,但在運(yùn)行時(shí)會(huì)被動(dòng)態(tài)綁定到其實(shí)際類(lèi)型Son并調(diào)用到正確的被重寫(xiě)后的函數(shù),這是多態(tài),通過(guò)調(diào)用函數(shù)的對(duì)象執(zhí)行動(dòng)態(tài)綁定。在主流語(yǔ)言,比如C#, C++ 和 JAVA中,編譯器在編譯類(lèi)函數(shù)的時(shí)候會(huì)進(jìn)行擴(kuò)充,把this指針隱含的傳遞到方法里面,上面的方法會(huì)擴(kuò)充為
void DoSomething(this, string); void DoSomething(this, string);
在多態(tài)中實(shí)現(xiàn)的this指針動(dòng)態(tài)綁定,其實(shí)是針對(duì)函數(shù)的第一個(gè)參數(shù)進(jìn)行運(yùn)行時(shí)動(dòng)態(tài)綁定,這個(gè)也是單分派的定義。
至于雙分派,顧名思義,就是可以針對(duì)兩個(gè)參數(shù)進(jìn)行運(yùn)行時(shí)綁定的分派方法,不過(guò)可惜,C#等都不支持,所以大家現(xiàn)在應(yīng)該能理解為什么上面的代碼不能通過(guò)編譯了吧,上面的代碼通過(guò)編譯器的擴(kuò)充,變成了
public void Update(DocumentStatus status, ImageElement imageElement) public void Update(DocumentStatus status, ParagraphElement imageElement)
因?yàn)镃#不支持雙分派,第二參數(shù)無(wú)法動(dòng)態(tài)解析,所以就算實(shí)際類(lèi)型是ImageElement,但是聲明類(lèi)型是其基類(lèi)DocumentElement,也會(huì)被編譯器拒絕。
所以,為了在本不支持雙分派的C#中實(shí)現(xiàn)雙分派,我們需要添加一個(gè)跳板函數(shù),通過(guò)這個(gè)函數(shù),我們讓第二參數(shù)充當(dāng)被調(diào)用對(duì)象,實(shí)現(xiàn)動(dòng)態(tài)綁定,從而找到正確的重載函數(shù),我們需要引出今天的主角,Visitor模式。
Visitor模式
Visitor is a behavioral design pattern that lets you separate algorithms from the objects on which they operate.
翻譯的更直白一點(diǎn),Visitor模式允許針對(duì)不同的具體類(lèi)型定制不同的訪問(wèn)方法,而這個(gè)訪問(wèn)者本身,也可以是不同的類(lèi)型,看一下UML
在Visitor模式中,我們需要把訪問(wèn)者抽象出來(lái),以方便之后定制更多的不同類(lèi)型的訪問(wèn)者
抽象出DocumentElementVisitor,含有兩個(gè)版本的Visit方法,在其子類(lèi)中具體定制針對(duì)不同類(lèi)型的訪問(wèn)方法
public abstract class DocumentElementVisitor { public abstract void Visit(ImageElement imageElement); public abstract void Visit(ParagraphElement imageElement); } public class DocumentStatus : DocumentElementVisitor { public int CharNum { get; set; } public int WordNum { get; set; } public int ImageNum { get; set; } public void ShowStatus() { Console.WriteLine("I have {0} char, {1} word and {2} image", CharNum, WordNum, ImageNum); } public void Update(DocumentElement documentElement) { documentElement.Accept(this); } public override void Visit(ImageElement imageElement) { ImageNum++; } public override void Visit(ParagraphElement paragraphElement) { WordNum += paragraphElement.WordNum; CharNum += paragraphElement.CharNum; } }
在被訪問(wèn)類(lèi)的基類(lèi)中添加一個(gè)Accept方法,這個(gè)方法用來(lái)實(shí)現(xiàn)雙分派,這個(gè)方法就是我們前文提到的跳板函數(shù),它的作用就是讓第二參數(shù)充當(dāng)被調(diào)用對(duì)象,第二次利用多態(tài)(第一次多態(tài)發(fā)生在調(diào)用Accept方法的時(shí)候)
public abstract class DocumentElement { public abstract void Accept(DocumentElementVisitor visitor); } public class ImageElement : DocumentElement { public override void Accept(DocumentElementVisitor visitor) { visitor.Visit(this); } } public class ParagraphElement : DocumentElement { public int CharNum { get; set; } public int WordNum { get; set; } public ParagraphElement(int charNum, int wordNum) { CharNum = charNum; WordNum = wordNum; } public override void Accept(DocumentElementVisitor visitor) { visitor.Visit(this); } }
這里,Accept方法就是Visitor模式的精髓,通過(guò)調(diào)用被訪問(wèn)基類(lèi)的Accept方法,被訪問(wèn)基類(lèi)通過(guò)語(yǔ)言的單分派,動(dòng)態(tài)綁定了正確的被訪問(wèn)子類(lèi),接著在子類(lèi)方法中,將第一參數(shù)當(dāng)做執(zhí)行對(duì)象再調(diào)用一次它的方法,根據(jù)語(yǔ)言的單分派機(jī)制,第一參數(shù)也能被正確的動(dòng)態(tài)綁定類(lèi)型,這樣就實(shí)現(xiàn)了雙分派
這就是Visitor模式的簡(jiǎn)單介紹,這個(gè)模式的好處在于:
- 克服語(yǔ)言沒(méi)有雙分派功能的缺陷,能夠正確的解析參數(shù)的類(lèi)型,尤其當(dāng)想要對(duì)一個(gè)繼承族群類(lèi)的不同子類(lèi)定制訪問(wèn)方法時(shí),這個(gè)模式可以派上用場(chǎng)
- 非常便于添加訪問(wèn)者,試想,如果我們未來(lái)想要添加一個(gè)DocumentPriceCount,需要對(duì)段落和圖片計(jì)費(fèi),我們只需要新建一個(gè)類(lèi),繼承自DocumentVisitor,同時(shí)實(shí)現(xiàn)相應(yīng)的Visit方法就行
希望大家通過(guò)這篇文章,能對(duì)Visitor模式有一定了解,在實(shí)踐中可以恰當(dāng)?shù)氖褂谩?/p>
總結(jié)
到此這篇關(guān)于C#中Visitor模式的文章就介紹到這了,更多相關(guān)C# Visitor模式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#利用win32 Api 修改本地系統(tǒng)時(shí)間、獲取硬盤(pán)序列號(hào)
這篇文章主要介紹了C#利用win32 Api 修改本地系統(tǒng)時(shí)間、獲取硬盤(pán)序列號(hào)的方法及代碼分享,需要的朋友可以參考下2015-03-03C#控制臺(tái)程序中使用官方依賴(lài)注入的實(shí)現(xiàn)
這篇文章主要介紹了C#控制臺(tái)程序中使用官方依賴(lài)注入的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04分享WCF文件傳輸實(shí)現(xiàn)方法---WCFFileTransfer
這篇文章主要介紹了分享WCF文件傳輸實(shí)現(xiàn)方法---WCFFileTransfer,需要的朋友可以參考下2015-11-11WPF設(shè)置窗體可以使用鼠標(biāo)拖動(dòng)大小的方法
這篇文章主要介紹了WPF設(shè)置窗體可以使用鼠標(biāo)拖動(dòng)大小的方法,涉及針對(duì)窗口的操作與設(shè)置技巧,具有很好的借鑒價(jià)值,需要的朋友可以參考下2014-11-11C#中方法的直接調(diào)用、反射調(diào)用與Lambda表達(dá)式調(diào)用對(duì)比
這篇文章主要介紹了C#中方法的直接調(diào)用、反射調(diào)用與Lambda表達(dá)式調(diào)用對(duì)比,本文著重講解了方法的三種調(diào)用方法以及它們的性能對(duì)比,需要的朋友可以參考下2015-06-06輕松學(xué)習(xí)C#的基礎(chǔ)入門(mén)
輕松學(xué)習(xí)C#的基礎(chǔ)入門(mén),了解C#最基本的知識(shí)點(diǎn),C#是一種簡(jiǎn)潔的,類(lèi)型安全的一種完全面向?qū)ο蟮拈_(kāi)發(fā)語(yǔ)言,是Microsoft專(zhuān)門(mén)基于.NET Framework平臺(tái)開(kāi)發(fā)的而量身定做的高級(jí)程序設(shè)計(jì)語(yǔ)言,需要的朋友可以參考下2015-11-11