聊聊 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.mzph.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独立按住啊或向现有安装添加功能】。 点击【下一步】。

《看聊天记录都学不会C#?太菜了吧》(4)C# 中的尚方宝剑 “先斩后奏”

本系列文章将会以通俗易懂的对话方式进行教学&#xff0c;对话中将涵盖了新手在学习中的一般问题。此系列将会持续更新&#xff0c;包括别的语言以及实战都将使用对话的方式进行教学&#xff0c;基础编程语言教学适用于零基础小白&#xff0c;之后实战课程也将会逐步更新。 若…

如何用 Swift 语言构建一个自定控件

本文译自&#xff1a;How To Make a Custom Control in Swift 用户界面控件是所有应用程序重要的组成部分之一。它们以图形组件的方式呈现给用户&#xff0c;用户可以通过它们与应用程序进行交互。苹果提供了一套控件&#xff0c;例如 UITextField&#xff0c;UIButton&#xf…

《看聊天记录都学不会C#?太菜了吧》(5)C# 中可以用中文名变量?

本系列文章将会以通俗易懂的对话方式进行教学&#xff0c;对话中将涵盖了新手在学习中的一般问题。此系列将会持续更新&#xff0c;包括别的语言以及实战都将使用对话的方式进行教学&#xff0c;基础编程语言教学适用于零基础小白&#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#?太菜了吧》(6)多晦涩的专业术语原来都会那么简单

本系列文章将会以通俗易懂的对话方式进行教学&#xff0c;对话中将涵盖了新手在学习中的一般问题。此系列将会持续更新&#xff0c;包括别的语言以及实战都将使用对话的方式进行教学&#xff0c;基础编程语言教学适用于零基础小白&#xff0c;之后实战课程也将会逐步更新。 若…

SQLServer2008-镜像数据库实施手册(双机)SQL-Server2014同样适用

SQL Server2008R2-镜像数据库实施手册(双机)SQL Server2014同样适用 一、配置主备机 1、 服务器基本信息 主机名称为&#xff1a;HOST_A&#xff0c;IP地址为&#xff1a;192.168.1.155 备机名称为&#xff1a;HOST_B&#xff0c;IP地址为&#xff1a;192.168.1.156 二、主备实…

一万字一篇文20分钟学会C语言和Python,十四年编程经验老鸟传授经验之道

前言 昨天在直播中有粉丝问我如何快速的对编程语言入门&#xff0c;我想这个问题是有必要让大家知道的&#xff0c;相必也有很多新手对于如何快速完成编程语言的入门学习很感兴趣&#xff0c;本篇文将会使用 C 语言以及 Python 为例&#xff0c;做出对比&#xff0c;让大家对编…

【Python可视化】Windows 10系统上Pyecharts安装教程

简单的Python库&#xff0c;如Numpy&#xff0c;可以直接在PyCharm中自动下载并安装。 同添加Python环境变量一样&#xff0c;需要先添加pip环境变量。pip位于C:\Python27\ArcGIS10.8\Scripts路径下。 WinR→cmd&#xff1a; 安装完成&#xff01;

使用.Net分析.Net达人挑战赛参与情况

背景C#是我2012年在大学课程中接触的&#xff0c;.NET Framework 我也一直使用至今。从2014年.NET 开源&#xff0c;2019年发布.NET Core 3 的时候&#xff0c;公司刚好有 Nvidia Jetson 平台 Linux 嵌入式设备的开发任务&#xff0c;.NET 又刚是适用于 Windows, Linux, 和 mac…

十分钟如何学会C语言?掌握规律举一反三考试提50分!

前言 上周写了一篇 20 分钟学会 C 语言与Python的文章——《一万字一篇文20分钟学会C语言和Python&#xff0c;十四年编程经验老鸟传授经验之道》&#xff0c;之后见粉丝转了一个话题“十分钟如何学会C语言”&#xff0c;我就在想是否能够十分钟呢&#xff1f;答案是可以的&am…

c语言在win8系统不兼容,Win8系统中存在不兼容软件如何解决?

最近有刚升级Win8系统的用户反映&#xff0c;FastStone Capture截图软件在Win7系统中可以兼容&#xff0c;正常打开&#xff0c;可是在Win8系统中就不能兼容了&#xff0c;这让用户非常烦恼。那么&#xff0c;Win8系统中存在不兼容软件如何解决呢&#xff1f;下面&#xff0c;我…

Python 3.6出现报错解决方案:No Python 3.6 installation was detected,无法卸载Python

卸载Python 3.6时错误提示&#xff0c;No Python 3.6 installation was detected。 解决办法是&#xff0c;先右键→更改→Repair。 然后再卸载&#xff0c;完成&#xff01;

MASA Auth - 权限设计

权限术语Subject&#xff1a;用户&#xff0c;用户组Action&#xff1a;对Object的操作&#xff0c;如增删改查等Object&#xff1a;权限作用的对象&#xff0c;也可以理解为资源Effect&#xff1a;规则的作用&#xff0c;如允许&#xff0c;拒绝Condition&#xff1a;生效条件…

【必懂】C语言水仙花数题解

若是大一学子或者是真心想学习刚入门的小伙伴可以私聊我&#xff0c;若你是真心学习可以送你书籍&#xff0c;指导你学习&#xff0c;给予你目标方向的学习路线&#xff0c;无套路&#xff0c;博客为证。 前言 本专栏内容将会以轻松、简单的方式完成习题的解答&#xff0c;用…