学了那么久的Java,你是否知道Java是属于单分派语言还是双分派语言?什么?单分派和双分派是什么意思还不知道?了解了分派机制,就能明白访问者模式的前世今生了。

访问者模式是23种设计模式当中比较少见少用的一种,相比于其他常见的,如单例、工厂、观察者、代理等模式,理解起来要稍微费劲一点。但,如果从访问者模式的产生由来去思考和理解,或许会更容易,那为什么会出现访问者这一种设计模式呢?首先先要理解什么是单分派和双分派。

分派

分派是什么样的概念呢?分派即Dispatch,在面向对象编程语言中,我们可以把方法调用理解为一种消息传递(Dispatch)。一个对象调用另一个对象的方法,相当于给被调用对象发送一个消息,这个消息包括对象名、方法名、方法参数等信息。

单分派

单分派,即执行哪个对象的方法,根据对象的运行时类型决定;执行对象的哪个方法,根据方法参数的编译时类型决定。

双分派

双分派,即执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的运行时的类型来决定。

听起来似乎很绕口,其实很简单,下面以表格的形式展示,可能会更加明了一点。

分派机制

分派机制如果在是在编程语言中,单分派和双分派就是跟多态和函数重载直接相关。那Java是属于单分派还是双分派呢?我们直接用个代码示例在验证好了。

public class Parent {

public void call() {        System.out.println("I'm Parent.");    }

}
public class Child extends Parent {

public void call() {        System.out.println("I'm Child.");    }

}
public class App {

public void call(Parent parent) {        parent.call();    }

public  void sayName(Parent parent) {        System.out.println("saveName重载,参数类型: Parent");    }

public  void sayName(Child child) {        System.out.println("saveName重载,参数类型: Child");    }

public static void main(String[] args) {        App app = new App();        Parent obj = new Child();        app.call(obj);

        app.sayName(obj);    }

}

你觉得上述代码的执行结果输出是什么呢?如下:

I'm Child.saveName重载,参数类型: Parent

Process finished with exit code 0

简单分析下:

new对象时,我们new的是Child对象,通过call方法的调用结果可知,最终打印出的是I'm Child.,即App类的call方法中,到底调用Parent的call还是会调用Child的call,由我们创建的对象实例类型决定,是运行时决定的,正所谓多态;而通过saveName方法的调用结果可知,尽管我们new的是Child对象,结果调用的却是形参类型是Parent的那个重载,由此可知,这里决定调用那个saveName重载,在编译时就决定了。所以,很明显了,「Java支持单分派,不支持多分派」

也因此得知,Java语言中的函数重载,并不是在运行时,根据传递给函数的参数的实际类型,来决定调用哪个重载函数,而是在编译时,根据传递给函数的参数的声明类型,来决定调用哪个重载函数。即Java语言中,「多态是一种动态绑定,函数重载是一种静态绑定」

访问者模式的由来

正因为在某些编程语言中,如Java不支持双分派,所以访问者设计模式诞生了。何出此言?我们先来看一个业务功能场景。

假设有一个图像视频存储服务器,这个服务器需要针对用户上传的图像、视频进行合法性校验,如不能涉及黄色内容,也是鉴黄拦截处理。而图像视频有多种封装格式,如有图片jpg、png,动图gif,视频mp4、avi、flv等,然后不同类型的文件,提取内容的方式也不一样,下面是针对这个功能场景的一个代码设计。

如果你有一定的代码架构设计基础,相信一开始你也能想到可以利用多态特性来实现比较友好的封装抽象设计,如下:

「#基本数据类」

/** * 媒体文件基类 */public abstract class MediaFile {protected String filePath;public MediaFile(String filePath) {this.filePath = filePath;    }}

/** * 假设这个Image类是从不同媒体文件中提取出来的对象, * 里面存储图像鉴别的相关内容和信息,此处省略相关变量和方法 */public class Image {}

/** * 该类代表静态图片,如jpg、png */public class Picture extends MediaFile {public Picture(String filePath) {super(filePath);    }}

/** * 此类表示动态图,如gif */public class Gif extends MediaFile {public Gif(String filePath) {super(filePath);    }}

/** * 此类代表视频媒体文件,如MP4、AVI等 */public class Video extends MediaFile {public Video(String filePath) {super(filePath);    }}

「#图像内容提取器」

/** * 为了以后更好的扩展,如针对媒体文件可能还会有其他操作,而不仅仅是提取图像内容 * 因此,我们把提取图像内容单独分离出来,弄成一个提取器 */public class Extractor {

public void extract(Picture picture) {        Image image = new Image();        System.out.println("提取静态图片[" + picture.filePath + "]中的图像信息");// 省略:image对象数据填充操作,图像鉴别操作    }

public void extract(Gif gif) {        Image image = new Image();        System.out.println("提取动态图片[" + gif.filePath + "]中的图像信息");// 省略:image对象数据填充操作,图像鉴别操作    }

public void extract(Video video) {        Image image = new Image();        System.out.println("提取视频[" + video.filePath + "]中的图像信息");// 省略:image对象数据填充操作,图像鉴别操作    }}

「#鉴别处理」

public class App {public static void main(String[] args) {        Extractor extractor = new Extractor();        List mediaFiles = Arrays.asList(new Picture("a.jpg"),new Picture("b.png"),new Gif("c.gif"),new Video("d.mp4"),new Video("e.avi")        );for (MediaFile media : mediaFiles) {            extractor.extract(media);        }    }}

代码写到这里,看起来应该没有什么问题,整体的设计也比较符合开闭原则,唯一一点不太优雅的就是Extractor提取器,如果日后新增一种媒体类型文件,就得增加一个重载方法,但这问题不大,整体设计也相当灵活了。

但是,如果你没自己去在IDEA写这个代码的话,第一眼很难看出,其实上面的代码有一处地方是编译不通过的。就是主函数入口中,for循环里的这句代码:

Image image = extractor.extract(media);

在这个for循环中,media是MediaFile基类的引用类型,而在Extractor中,并没有针对MediaFile基类的重载方法,而又由于Java是不支持双分派的,因此这里是编译不通过的。

有点可惜,挺好架构设计,却行不通,那该如何解决这个问题呢?其实,懂的自然懂,不懂的,也不容易想到这种设计,就是MediaFile基类中新增一个抽象方法,该方法接收Extractor作为形参,表示提取操作,然后子类重写该方法,执行相应的Extractor的重载即可,重新设计架构后的代码如下:

「#基本数据类」

public abstract class MediaFile {protected String filePath;public MediaFile(String filePath) {this.filePath = filePath;    }

public abstract void accept(Extractor extractor);}

public class Image {}

public class Picture extends MediaFile {public Picture(String filePath) {super(filePath);    }

@Overridepublic void accept(Extractor extractor) {        extractor.extract(this);    }}

public class Gif extends MediaFile {public Gif(String filePath) {super(filePath);    }

@Overridepublic void accept(Extractor extractor) {        extractor.extract(this);    }}

public class Video extends MediaFile {public Video(String filePath) {super(filePath);    }

@Overridepublic void accept(Extractor extractor) {        extractor.extract(this);    }}

Extractor保持原有设计不变,主函数调用如下:

public class App {public static void main(String[] args) {// 此处省略,跟第一种设计里的代码一样for (MediaFile media : mediaFiles) {            media.accept(extractor);        }    }}

到这里,编译通过, 看一下程序运行结果:

提取静态图片[a.jpg]中的图像信息提取静态图片[b.png]中的图像信息提取动态图片[c.gif]中的图像信息提取视频[d.mp4]中的图像信息提取视频[e.avi]中的图像信息

Process finished with exit code 0

其实,到这里,你就已经知道了什么是访问者模式了,上述的第二种设计思路,就是访问者模式的设计思路,只不过不是完整版的访问者模式,只能算是简版的。为什么这么说呢?我们都知道,每一个设计模式的诞生,都是有某一种业务功能场景需求作为背景的,而设计模式则是为了更好的对代码结构进行优化,使其变得更加解耦、易维护、可扩展。上述的简版访问者模式代码设计中,有一个很严重的弊端:

如果日后需要对媒体文件新增一种功能操作,如给媒体文件添加水印,那么MediaFile基类,就得新增一个抽象方法,如:

public abstract class MediaFile {

protected String filePath;

public MediaFile(String filePath) {this.filePath = filePath;    }

// 提取图像内容public abstract void accept(Extractor extractor);// 添加水印public abstract void accept(Watermarker watermarker);}

同时它的所有子类都得重写多这个方法,违反了开闭原则。

接下来是该完整版的访问者模式登场了。

「#定义一个访问者接口或者基类」

/** * 访问者接口,具体如何访问,由其实现类定义 */public interface Visitor {void visit(Picture picture);void visit(Gif gif);void visit(Video video);}

「#基本数据类」

// 媒体文件基类public abstract class MediaFile {protected String filePath;public MediaFile(String filePath) {this.filePath = filePath;    }public abstract void accept(Visitor visitor);}

// 静态图片public class Picture extends MediaFile {public Picture(String filePath) {super(filePath);    }

@Overridepublic void accept(Visitor visitor) {         visitor.visit(this);    }}

// 动态图片public class Gif extends MediaFile {public Gif(String filePath) {super(filePath);    }

@Overridepublic void accept(Visitor visitor) {        visitor.visit(this);    }}

// 视频public class Video extends MediaFile {public Video(String filePath) {super(filePath);    }

@Overridepublic void accept(Visitor visitor) {        visitor.visit(this);    }}

「#图像信息提取并鉴别访问者」

public class Extractor implements Visitor {

public void visit(Picture picture) {        Image image = new Image();        System.out.println("提取静态图片[" + picture.filePath + "]中的图像信息");// 省略:image对象数据填充操作,图像鉴别操作    }

public void visit(Gif gif) {        Image image = new Image();        System.out.println("提取动态图片[" + gif.filePath + "]中的图像信息");// 省略:image对象数据填充操作,图像鉴别操作    }

public void visit(Video video) {        Image image = new Image();        System.out.println("提取视频[" + video.filePath + "]中的图像信息");// 省略:image对象数据填充操作,图像鉴别操作    }}

「#添加水印访问者」

public class Watermarker implements Visitor {@Overridepublic void visit(Picture picture) {        System.out.println("给静态图片[" + picture.filePath + "]添加水印");    }

@Overridepublic void visit(Gif gif) {        System.out.println("给动态图片[" + gif.filePath + "]添加水印");    }

@Overridepublic void visit(Video video) {        System.out.println("给视频[" + video.filePath + "]添加水印");    }}

「#执行函数」

public class App {public static void main(String[] args) {        List mediaFiles = Arrays.asList(new Picture("a.jpg"),new Picture("b.png"),new Gif("c.gif"),new Video("d.mp4"),new Video("e.avi")        );        Extractor extractor = new Extractor();for (MediaFile media : mediaFiles) {            media.accept(extractor);        }        Watermarker watermarker = new Watermarker();for (MediaFile media : mediaFiles) {            media.accept(watermarker);        }    }}

「#程序执行结果」

提取静态图片[a.jpg]中的图像信息提取静态图片[b.png]中的图像信息提取动态图片[c.gif]中的图像信息提取视频[d.mp4]中的图像信息提取视频[e.avi]中的图像信息给静态图片[a.jpg]添加水印给静态图片[b.png]添加水印给动态图片[c.gif]添加水印给视频[d.mp4]添加水印给视频[e.avi]添加水印

Process finished with exit code 0

可见,完整版的访问者模式,大大提高代码的可扩展性,使其更解耦、更易维护,假如日后需要新增一种媒体文件操作,直接新增一个访问者,在这个访问者类里实现相关逻辑操作即可。

细心的你,可能也会发现,访问者模式也并不是没有缺点,比如,如果要处理的媒体类型文件不止三种,那么每一个访问者都得相应增加对应的媒体类型文件的操作方法。是的,这是不可避免的,事实上,也不存在一种十全十美的设计模式,每一种设计模式都必然存在着缺点和优点,就像一把双刃剑。我们需要明白的是,在代码架构设计中,设计模式只是一种工具,一种辅助性的东西,而真正帮助我们设计更好的代码架构,是设计模式中背后的那些设计思想、设计原理,知道这个设计模式怎么来,可能会比如何使用这个设计模式更加重要,正所谓授人以鱼不如授人以渔,同时我们也该知道,每一种设计模式,都是在某一种业务场景需求下诞生的,是因为产生了问题,才会有解决问题的办法。

总结

这里总结一下,如果某一种编程语言支持双分派,也就不需要访问者模式了,访问者模式,相比于上述编译不通过的代码,还更复杂些。而访问者模式,它的本质是针对相同的内容或信息,不同的访问者可以做不同的处理,对于上述的业务场景,当然也可以有其他的设计方案,如策略模式,但在这里,访问者模式可能更适合一些,策略模式更偏向于同样的操作,不同的算法。不过,如果你已经掌握访问者模式背后的设计思想,在实际应用中,不论你的设计是不是真正的访问者模式,能应用上其背后的设计思想,就已经达到目的了。


「THE END」

另外,不妨关注一下公众号,一起聊聊技术,聊聊职场,聊聊人生。


cacheinterceptor第二次访问没被调用_双分派访问者模式的前世今生相关推荐

  1. cacheinterceptor第二次访问没被调用_访问者设计模式在OSG中的应用

    为什么要谈谈访问者设计模式呢?因为OSG整个引擎就是用访问者设计模式建立起来的,不论是遍历节点图,还是做各种实用的功能,都需要大量的用到访问者设计模式. 先谈谈访问者设计模式的定义. 1:什么是访问者 ...

  2. TF卡里删掉文件后内存没变大_双11,TF卡,SD卡,读卡器如何选,看这篇就够了...

    此文章发布已经半年有余,各大厂家推出了很多新的SD卡,且SD卡组织也推出了新的标准,所以这篇文章的内容已经有些过时,还得烦请各位移步到新的文章: 黄昏百分百:TF卡,SD卡,读卡器,USB拓展坞如何选 ...

  3. java 双分派_双分派 和 访问者模式详解 | 学步园

    为什么 网上的人都说 java 只支持 单分派不支持双分派? 这段代码摘子某书[code=Java] public class Dispatch{ static class QQ{} static c ...

  4. java 访问者模式_设计模式之访问者模式

    public interface Visitor { public void visitString(StringElement stringE); public void visitFloat(Fl ...

  5. github private链接访问_Hands-On Design Patterns With C++(十八)访问者模式与多分派(下)...

    本文是本书最后一篇文章,完结撒花!谢谢大家观看! 目录: trick:Hands-On Design Patterns With C++(零)前言​zhuanlan.zhihu.com 访问者模式与多 ...

  6. jsp调用controller方法_RPC调用_服务注册与发现

    RPC调用_单体架构_SOA架构 系统架构的演变 1 传统的单体架构 1.1 什么是单体架构 一个归档包(例如 war 格式或者 Jar 格式)包含了应用所有功能的应用程序,我们通常称之 为单体应用. ...

  7. python 私有变量怎么调用_我的Python学习笔记(三):私有变量

    一.私有变量的定义 在Python中,有以下几种方式来定义变量: xx:公有变量 _xx:单前置下划线,私有化属性或方法,类对象和子类可以访问,from somemodule import *禁止导入 ...

  8. 【黑马-SpringCloud技术栈】【02】服务拆分及远程调用_服务提供者与消费者

    持续学习&持续更新中- 守破离 [黑马-SpringCloud技术栈][02]服务拆分及远程调用_服务提供者与消费者 SpringCloud引入 服务拆分及远程调用 服务拆分原则 服务拆分De ...

  9. go语言之进阶篇主协程先退出导致子协程没来得及调用

    1.主协程先退出导致子协程没来得及调用 示例: package mainimport ("fmt""time" )//主协程退出了,其它子协程也要跟着退出 fu ...

最新文章

  1. Windows 终端神器 MobaXterm,免费版可以在公司环境下使用
  2. 开发板与pc之间文件传输:kermit and lrzsz
  3. Python常用模块——目录
  4. 新手向:Vue 2.0 的建议学习顺序
  5. 专家建议:维护边缘网络安全的五项原则
  6. VM虚拟机里,如何将Linux Ubuntu系统改为简体中文及下载拼音的打字法
  7. python利用faker,输出企业名称、用户名称、手机号、地址信息等测试数据实例
  8. 机器学习预测信贷风险
  9. 资源放送丨《Oracle聚簇因子的作用 - 2020云和恩墨大讲堂》PPT视频
  10. java通过各种类型驱动连接数据库
  11. STM32+uCOS-II+uc/GUI移植 (uC/GUI API函数学习一)
  12. 进程、线程和协程详解
  13. 管桩的弹性模量计算公式_400管桩单桩水平承载力特征值计算书
  14. 苹果发布 AI 生成模型 GAUDI,文字生成 3D 场景
  15. 2018福大软工实践第十二次作业
  16. sketch颜色和html颜色不一致,photoshop和sketch中图片色彩不一致的原因和解决办法...
  17. 找不到支撑位和压力位?看完本文可帮到你
  18. 防止电子元器件烧坏那些要避的坑
  19. 微型计算机的 I3 I5是,电脑i3和i5有什么区别
  20. python 面向对象三大特性

热门文章

  1. php 响应时间计算,计算每个请求的平均响应时间
  2. 使用screen的时候出现了如下错误: Cannot open your terminal '/dev/pts/0' - please check.
  3. 报错, liquibase.exception.ValidationFailedException: Validation Failed
  4. 安卓学习笔记11:常用布局 - 网格布局
  5. Java实训项目1:GUI学生信息管理系统 - 实训概述
  6. 【HDU1166】敌兵布阵,线段树练习
  7. c++获取子类窗口句柄位置_干货分享:用一百行代码做一个C/C++表白小程序,程序员的浪漫!...
  8. Intel Sandy Bridge/Ivy Bridge架构/微架构/流水线 (10) - 乱序引擎概述
  9. 日期格式无法识别_Excel – 将各种伪日期批量转化为真日期
  10. linux 下安装fbprophet