iis服务器助手广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >一起详细聊聊C#中的Visitor模式
  • 492
分享到

一起详细聊聊C#中的Visitor模式

2024-04-02 19:04:59 492人浏览 薄情痞子
摘要

目录写在前面模式演进举个例子使用了Tpye-Switch的版本尝试使用重载的版本单分派与双分派Visitor模式总结写在前面 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();
        }
    }

运行结果如下,非常简单

但是细看这版代码,会发现有以下问题:

  • 所有的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

在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模式有一定了解,在实践中可以恰当的使用。

总结

到此这篇关于C#中Visitor模式的文章就介绍到这了,更多相关C# Visitor模式内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: 一起详细聊聊C#中的Visitor模式

本文链接: https://www.lsjlt.com/news/149309.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 一起详细聊聊C#中的Visitor模式
    目录写在前面模式演进举个例子使用了Tpye-Switch的版本尝试使用重载的版本单分派与双分派Visitor模式总结写在前面 Visitor模式在日常工作中出场比较少,如果统计大家不...
    99+
    2024-04-02
  • 详细聊聊Vue中的MVVM模式原理
    目录1. MVVM模式2. Vue响应式3. Vue监听对象3.1 监听普通对象3.2 监听复杂对象(深度监听)4. Vue监听数组5. 使用 Object.defineProper...
    99+
    2023-03-03
    vue.js mvvm vue.js教程 vue mvvm模式
  • 一起聊聊C++中的智能指针
    目录一:背景二:关键词解析1. auto_ptr2. auto_ptr 多引用问题一:背景 我们知道 C++ 是手工管理内存的分配和释放,对应的操作符就是 new/dele...
    99+
    2024-04-02
  • 详细聊聊JDK中的反模式接口常量
    目录前言常量接口类接口枚举类型结束语前言 在实际开发过程中,经常会需要定义一个文件,用于存储一些常量,这些常量设计为静态公共常量(使用 public static fina...
    99+
    2024-04-02
  • 一起聊聊C++中的特殊成员函数
    目录一:背景二:特殊成员函数1. 默认构造函数2. 析构函数3. 赋值构造函数4. 赋值运算符一:背景 在C#中要说类默认给我们定义的特殊成员函数,莫过于构造函数,但在 C++ 中这...
    99+
    2024-04-02
  • 一起聊聊C++中的四种类型转换符
    目录一:背景二:理解四大运算符1. const_cast2. reinterpret_cast3. dynamic_cast3. static_cast一:背景 在玩 C 的时候,经...
    99+
    2024-04-02
  • 详细聊一聊algorithm中的排序算法
    目录前言一、algorithm是什么?二、有哪些排序算法?sortrandom_shufflemergereverse总结前言 雨下不停,爱意难眠,说一下algorithm中的几个排...
    99+
    2024-04-02
  • 详细聊聊Vue.js中的MVVM
    目录MVVM的理解MVVM的原理脏检查机制:数据劫持相同点实现MVVM总结MVVM的理解 MVVM拆开来即为Model-View-ViewModel,有View,ViewModel...
    99+
    2024-04-02
  • 一起聊聊Java中13种锁的实现方式
    目录1、悲观锁2、乐观锁3、分布式锁加锁4、可重入锁5、自旋锁6、独享锁7、共享锁8、读锁/写锁9、公平锁/非公平锁10、可中断锁/不可中断锁11、分段锁12、锁升级(无锁|偏向锁|...
    99+
    2022-11-13
    Java 实现锁 Java 锁
  • 详细聊一聊java中封装的那点事
    目录什么是封装封装拓展之包包的概念 什么是包访问权限什么是静态成员总结一下: 什么是封装 什么是封装呢?我们先来看一段代码 class Student { ...
    99+
    2024-04-02
  • 一起聊聊JavaScript中的声明提升
    以上就是一起聊聊JavaScript中的声明提升的详细内容,更多请关注编程网其它相关文章!...
    99+
    2022-11-22
    JavaScript
  • 详细聊聊JS中不一样的深拷贝
    前言 对于深拷贝这个概念在面试中时常被提起,面试官可能让你实现深拷贝需要考虑那些因素,或者直接让你手写封装一个深拷贝,那么今天就和大家探讨一下一个让面试官感到牛的深拷贝, 1.思考 ...
    99+
    2022-11-13
    js深拷贝实现方式 js 对象深拷贝 js深拷贝的应用场景
  • 一起聊聊Node中的事件循环
    事件循环是 Node.js 的基本组成部分,通过确保主线程不被阻塞来实现异步编程,了解事件循环对构建高效应用程序至关重要。下面本篇文章就来带大家深入了解Node中的事件循环 ,希望对大家有所帮助!你已经使用 Node.js 一段时间了,构建...
    99+
    2023-05-14
    Node.js 前端 JavaScript
  • 详细聊聊c语言中的缓冲区问题
    目录发现问题例题问题原因解决方法一:解决方法二:解决方案三:出错二gets函数引入为什么要引入缓冲区总结发现问题 你是不是总会出现当你输入的时候(你想的是只输出一个内容),但是最后却...
    99+
    2024-04-02
  • 详细聊聊Vue中的options选项
    目录Vue中的options选项options的五类属性入门属性使用vue文件添加组件computed(计算属性)用途缓存示例:watch(监听)用途何为变化deep: true是干...
    99+
    2024-04-02
  • 一文详细聊聊vue3的defineProps、defineEmits和defineExpose
    目录definePropsdefineEmitsdefineExpose总结 最近在开发中用到了vue3的defineProps、defineEmits和defineExp...
    99+
    2023-02-08
    vue3 defineProps vue3 defineEmits vue3 defineExpose
  • 详细聊聊MySQL中的LIMIT语句
    目录问题 server层和存储引擎层 那LIMIT是什么鬼? 怎么办? 吐个槽 最近有多个小伙伴在答疑群里问了小孩子关于LIMIT的一个问题,下边我来大致描述一下这个问题。 问题 ...
    99+
    2024-04-02
  • 详细聊聊Mybatis中万能的Map
    目录万能的Mapdemomap 实现add usermap 实现通过id查询多个参数可以使用Map进行传参总结万能的Map 假设,我们的实体类,或者数据库中的表,字段或者参数过多,我...
    99+
    2024-04-02
  • 一文聊聊node中的path模块
    path 模块是 nodejs 中用于处理文件/目录路径的一个内置模块,可以看作是一个工具箱,提供诸多方法供我们使用,当然都是和路径处理有关的。同时在前端开发中 path 模块出现的频率也是比较高的,比如配置 webpack 的时候等。本文...
    99+
    2023-05-14
    path模块 Node.js
  • 一文聊聊Node中的net模块
    而客户端和服务端的传输流如下如果角色变成发送者和接受者的时候,传输流如下图:可以看出来传输的过程中,从发送端开始,没经过一层协议都会加上所需要的首部信息.层层把关,层层加码. 然后到了接收端的时候, 就反而行之, 每经过一层都剥去对应的首部...
    99+
    2023-05-14
    net模块 node Node.js
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作