聊聊 C# 中的 Visitor 模式

? ?前言? ?

Visitor模式在日常工作中出場比較少,如果統計大家不熟悉的模式,那么它榜上有名的可能性非常大。使用頻率少,再加上很多文章提到Visitor模式都著重于它克服語言單分派的特點上面,而對何時應該使用這個模式及這個模式是怎么一點點演講出來的提之甚少,造成很多人對這個模式有種霧里看花的感覺,今天跟著老胡,我們一起來一點點揭開它的面紗吧。

? ?模式演進? ?

舉個例子

現在假設我們有一個簡單的需求,需要統計出一篇文檔中的字數、詞數和圖片數量。其中字數和詞數存在于段落中,圖片數量單獨統計。于是乎,我們可以很快的寫出第一版代碼

使用了基本抽象的版本

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();}}

運行結果如下,非常簡單

88409c008adacdb7a876bb7b84bd071b.png

但是細看這版代碼,會發現有以下問題:

?所有的DocumentElement派生類必須訪問DocumentStatus,根據迪米特法則,這不是個好現象,如果在未來對DocumentStatus有修改,這些派生類被波及的可能性極大
?統計代碼散落在不同的派生類里面,維護不方便

有鑒于此,我們推出了第二版代碼


? ?使用了Tpye-Switch的版本? ?

這一版代碼中,我們摒棄了之前在具體的DocumentElement派生類中進行統計的做法,直接在統計類中統一處理

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();}}

測試結果和第一個版本的代碼一樣,這一版代碼克服了第一個版本中,統計代碼散落,具體類依賴統計類的問題,轉而我們在統計類中集中處理了統計任務。但同時它引入了type-switch, 這也是一個不好的信號,具體表現在:

?代碼冗長且難以維護?如果派生層次加多,需要很小心的選擇case順序以防出現繼承層次較低的類出現在繼承層次更遠的類前面,從而造成后面的case永遠無法被訪問的情況,這造成了額外的精力成本

嘗試使用重載的版本

有鑒于上面type-switch版本的問題,作為敏銳的程序員,可能馬上有人就會提出重載方案:“如果我們針對每個具體的DocumentElement寫出相應的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();}}

看起來很好,不過可惜,這段代碼編譯失敗,編譯器會抱怨說,不能將DocumentElement轉為它的子類,這是為什么呢?講到這里,就不能不提一下編程語言中的單分派和雙分派


? ?單分派與雙分派? ?

大家都知道,多態是OOP的三個基本特征之一,即形如以下的代碼

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類型,但在運行時會被動態綁定到其實際類型Son并調用到正確的被重寫后的函數,這是多態,通過調用函數的對象執行動態綁定。在主流語言,比如C#, C++ 和 JAVA中,編譯器在編譯類函數的時候會進行擴充,把this指針隱含的傳遞到方法里面,上面的方法會擴充為

void?DoSomething(this,?string);void?DoSomething(this,?string);

在多態中實現的this指針動態綁定,其實是針對函數的第一個參數進行運行時動態綁定,這個也是單分派的定義。至于雙分派,顧名思義,就是可以針對兩個參數進行運行時綁定的分派方法,不過可惜,C#等都不支持,所以大家現在應該能理解為什么上面的代碼不能通過編譯了吧,上面的代碼通過編譯器的擴充,變成了

public?void?Update(DocumentStatus?status,?ImageElement?imageElement)public?void?Update(DocumentStatus?status,?ParagraphElement?imageElement)

因為C#不支持雙分派,第二參數無法動態解析,所以就算實際類型是ImageElement,但是聲明類型是其基類DocumentElement,也會被編譯器拒絕。所以,為了在本不支持雙分派的C#中實現雙分派,我們需要添加一個跳板函數,通過這個函數,我們讓第二參數充當被調用對象,實現動態綁定,從而找到正確的重載函數,我們需要引出今天的主角,Visitor模式。


? ?Visitor模式? ?

Visitor is a behavioral design pattern that lets you separate algorithms from the objects on which they operate.


翻譯的更直白一點,Visitor模式允許針對不同的具體類型定制不同的訪問方法,而這個訪問者本身,也可以是不同的類型,看一下UML

0eba57469b3f8ec5790e899fade7e478.png

在Visitor模式中,我們需要把訪問者抽象出來,以方便之后定制更多的不同類型的訪問者。

抽象出DocumentElementVisitor,含有兩個版本的Visit方法,在其子類中具體定制針對不同類型的訪問方法

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;}}

在被訪問類的基類中添加一個Accept方法,這個方法用來實現雙分派,這個方法就是我們前文提到的跳板函數,它的作用就是讓第二參數充當被調用對象,第二次利用多態(第一次多態發生在調用Accept方法的時候)

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模式的精髓,通過調用被訪問基類的Accept方法,被訪問基類通過語言的單分派,動態綁定了正確的被訪問子類,接著在子類方法中,將第一參數當做執行對象再調用一次它的方法,根據語言的單分派機制,第一參數也能被正確的動態綁定類型,這樣就實現了雙分派

這就是Visitor模式的簡單介紹,這個模式的好處在于:

?克服語言沒有雙分派功能的缺陷,能夠正確的解析參數的類型,尤其當想要對一個繼承族群類的不同子類定制訪問方法時,這個模式可以派上用場
?非常便于添加訪問者,試想,如果我們未來想要添加一個DocumentPriceCount,需要對段落和圖片計費,我們只需要新建一個類,繼承自DocumentVisitor,同時實現相應的Visit方法就行

希望大家通過這篇文章,能對Visitor模式有一定了解,在實踐中可以恰當的使用。?

673ddee888ca14902fc81cc547c8edc7.png

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/287149.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/287149.shtml
英文地址,請注明出處:http://en.pswp.cn/news/287149.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

AcWing 889. 滿足條件的01序列(卡特蘭數應用)

滿足條件的01序列 假設長度為n個序列要求滿足題意1的前綴0的個數不能超過1的個數 將問題抽象為從(0, 0)到(n, n) 向上走一個代表這一步對應序列中的值是1&#xff0c;向右走代表序列中的值是0 要想滿足1的前綴0的數量大于1的數量就需要滿足所有路過的途徑在y x這個函數個下面…

添加ASP.NET網站資源文件夾

ASP.NET應用程序包含7個默認文件夾&#xff0c;分別為Bin、APP_Code、App_GlobalResources、App_LocalResources、App_WebReferences、App_Browsers和“主題”文件夾。每個文件夾都存放ASP.NET應用程序的不同類型的資源。 方法 說明Bin  包含程序所需的所有已編譯程序集&#…

《看聊天記錄都學不會Python到游戲實戰?太菜了吧》(8)我們開始做一個數字小游戲吧

本系列文章將會以通俗易懂的對話方式進行教學&#xff0c;對話中將涵蓋了新手在學習中的一般問題。此系列將會持續更新&#xff0c;包括別的語言以及實戰都將使用對話的方式進行教學&#xff0c;基礎編程語言教學適用于零基礎小白&#xff0c;之后實戰課程也將會逐步更新。 若…

Microsoft SQL Server 2019開發版安裝配置教程

一、安裝cn_sql_server_2019_developer_x64 雙擊setup.exe進行安轉。 點擊【安裝】。 點擊【全新SQL Server獨立按住啊或向現有安裝添加功能】。 點擊【下一步】。

Git提示Please move or remove them before you switch branches.

1 問題 git checkout V1 提示錯誤如下 error: The following untracked working tree files would be overwritten by checkout:flutter_module/pubspec.lock Please move or remove them before you switch branches. Aborting2 解決辦法 git clean -df ../flutter_module…

c語言創建新指針,如何用c語言創建一個指針

您總是可以將指針強制轉換為整數&#xff0c;即整數大小比系統中使用的字節指針大3位。然后在向左移動3位后移動指針。然后將位信息存儲在最低有效3位上。然后可以用正常算術遞增該整數“位指針”。像這樣的東西&#xff1a;#include #define bitptr long long#define create_b…

請查收最新的 EF Core 7.0 更新

關注我們作者&#xff1a;Jeremy Likness排版&#xff1a;Rani近期.NET 數據團隊宣布了 EF Core 7.0 (EF7)的第四個預覽版。除了bug修復和更大功能的基礎工作外&#xff0c;此預覽版還包括以確保轉換器和比較器由類型映射處理&#xff0c;并支持將轉換器與值生成器一起使用。請…

【CC精品教程】ContextCapture 4.4.12(CC,Smart 3D)簡體中文版安裝教程(附安裝包下載)

ContextCapture 4.4.12簡體中文版是一款功能強大的三維建模軟件,用戶只需使用自己拍攝的普通照片,就能快速創建細節豐富的三維實景模型,并在項目的整個生命周期內為設計、施工和運營決策提供精確的現實環境背景。 目 錄 一、安裝過程 1. 安裝主程序cncpc040412333en_updt1…

《看聊天記錄都學不會C#?太菜了吧》(4)C# 中的尚方寶劍 “先斬后奏”

本系列文章將會以通俗易懂的對話方式進行教學&#xff0c;對話中將涵蓋了新手在學習中的一般問題。此系列將會持續更新&#xff0c;包括別的語言以及實戰都將使用對話的方式進行教學&#xff0c;基礎編程語言教學適用于零基礎小白&#xff0c;之后實戰課程也將會逐步更新。 若…

Android之解決多語言適配部分TextView內容左對齊和內容一行不排滿就到第二行問題

1 問題 1、多語言適配部分TextView內容左對齊 2、內容一行不排滿就到第二行問題 2 解決辦法 問題1、在TextView里面加入下面參數 android:gravity="center" 問題2、 import android.content.Context; import android.graphics.Paint; import android.text.TextUti…

如何用 Swift 語言構建一個自定控件

本文譯自&#xff1a;How To Make a Custom Control in Swift 用戶界面控件是所有應用程序重要的組成部分之一。它們以圖形組件的方式呈現給用戶&#xff0c;用戶可以通過它們與應用程序進行交互。蘋果提供了一套控件&#xff0c;例如 UITextField&#xff0c;UIButton&#xf…

【ArcGIS遇上Python】ArcGIS Python獲取Shapefile矢量數據字段名稱

借助PyCharm環境&#xff0c;在不打開ArcGIS的情況下&#xff0c;編寫Python代碼&#xff0c;獲取矢量數據的所有字段。 import arcpyshp C:\data\out\Export_Output.shp fields arcpy.ListFields(shp) for f in fields:print f.name‘,’f.type運行結果&#xff1a; C:\Pyt…

《聰明人和傻子和程序員》

本文借鑒自魯迅雜文《聰明人和傻子和奴才》&#xff0c;如有雷同&#xff0c;純屬巧合。有個程序員特別喜歡尋人訴苦&#xff0c;只要一點事&#xff0c;就喜歡訴苦。有一日&#xff0c;他遇到一個聰明人。“大佬。”他悲哀的說&#xff0c;“我們公司待遇越來越差了&#xff0…

c語言 case語句用法,switch ... case語句的用法[組圖]

switch ... case語句的用法[組圖]08-13欄目&#xff1a;技術TAG&#xff1a;switch case語句switch case語句當情況大于或等于4種的時候就用switch ... case語句copyright jhua.orgswitch(表達式) copyright jhua.org{ https://www.jhua.orgcase 常量1&#xff1a; 語句體1&am…

《看聊天記錄都學不會C#?太菜了吧》(5)C# 中可以用中文名變量?

本系列文章將會以通俗易懂的對話方式進行教學&#xff0c;對話中將涵蓋了新手在學習中的一般問題。此系列將會持續更新&#xff0c;包括別的語言以及實戰都將使用對話的方式進行教學&#xff0c;基礎編程語言教學適用于零基礎小白&#xff0c;之后實戰課程也將會逐步更新。 若…

Android之TabLayout和ViewPager組合跳轉到指定頁面

1 問題 TabLayout和ViewPager組合跳轉到具體一個頁面 2 解決辦法 viewPager?.setCurrentItem(index) index為0說明是第一頁&#xff0c;如果是1的話就是第二頁&#xff0c;以此類推。

【ArcGIS遇上Python】ArcGIS Python中文編碼問題案例詳解

前面的文章《ArcGIS Python獲取Shapefile矢量數據字段名稱》我們已經學會了如何用 Python 獲取中文路徑下的shp數據的所有字段,英文沒有問題,但是如果你輸出中文路徑下的數據字段, 就有可能會碰到中文編碼問題。 Python 文件中如果未指定編碼,在執行過程會出現報錯: impo…

gRPC編碼初探(java)

背景&#xff1a;gRPC是一個高性能、通用的開源RPC框架&#xff0c;其由Google主要面向移動應用開發并基于HTTP/2協議標準而設計&#xff0c;基于ProtoBuf(Protocol Buffers)序列化協議開發&#xff0c;且支持眾多開發語言。gRPC提供了一種簡單的方法來精確地定義服務和為iOS、…

WPF 基礎控件之 RadioButton 樣式

其他基礎控件1.Window2.Button3.CheckBox4.ComboBox5.DataGrid 6.DatePicker7.Expander8.GroupBox9.ListBox10.ListView11.Menu12.PasswordBox13.TextBox14.ProgressBarRadioButton 實現下面的效果1&#xff09;RadioButton來實現動畫&#xff1b;Border嵌套 Ellipse并設置Sca…

對歸并排序進行c語言編程實現,歸并排序及C語言實現

排序系列之(1)歸并排序及C語言實現有很多算法在結構上是遞歸的&#xff1a;為了解決一個給定的問題&#xff0c;算法需要一次或多次遞歸的調用其本身來解決相關的問題。這些算法通常采用分治策略&#xff1a;將原問題劃分成n個規模較小而結構與原問題相似的子問題&#xff1b;遞…