《JAVA与模式》之访问者模式

系列文章目录


文章目录

  • 系列文章目录
  • 前言
  • 一、分派的概念
  • 二、分派的类型
  • 三、访问者模式的结构
  • 四、访问者模式的优点
  • 五、访问者模式的缺点


前言

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。
在这里插入图片描述
在阎宏博士的《JAVA与模式》一书中开头是这样描述访问者(Visitor)模式的:

访问者模式是对象的行为模式。访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构则可以保持不变。


一、分派的概念

变量被声明时的类型叫做变量的静态类型(Static Type),有些人又把静态类型叫做明显类型(Apparent Type);而变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)。比如:

List list = null;
list = new ArrayList();
  声明了一个变量list,它的静态类型(也叫明显类型)是List,而它的实际类型是ArrayList。

根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。

静态分派(Static Dispatch)发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。

动态分派(Dynamic Dispatch)发生在运行时期,动态分派动态地置换掉某个方法。

静态分派
  Java通过方法重载支持静态分派。用墨子骑马的故事作为例子,墨子可以骑白马或者黑马。墨子与白马、黑马和马的类图如下所示:
在这里插入图片描述
在这个系统中,墨子由Mozi类代表

复制代码
public class Mozi {

public void ride(Horse h){System.out.println("骑马");
}public void ride(WhiteHorse wh){System.out.println("骑白马");
}public void ride(BlackHorse bh){System.out.println("骑黑马");
}public static void main(String[] args) {Horse wh = new WhiteHorse();Horse bh = new BlackHorse();Mozi mozi = new Mozi();mozi.ride(wh);mozi.ride(bh);
}

}
复制代码
  显然,Mozi类的ride()方法是由三个方法重载而成的。这三个方法分别接受马(Horse)、白马(WhiteHorse)、黑马(BlackHorse)等类型的参数。

那么在运行时,程序会打印出什么结果呢?结果是程序会打印出相同的两行“骑马”。换言之,墨子发现他所骑的都是马。

为什么呢?两次对ride()方法的调用传入的是不同的参数,也就是wh和bh。它们虽然具有不同的真实类型,但是它们的静态类型都是一样的,均是Horse类型。

重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。

动态分派
  Java通过方法的重写支持动态分派。用马吃草的故事作为例子,代码如下所示:

复制代码
public class Horse {

public void eat(){System.out.println("马吃草");
}

}
复制代码
复制代码
public class BlackHorse extends Horse {

@Override
public void eat() {System.out.println("黑马吃草");
}

}
复制代码
复制代码
public class Client {

public static void main(String[] args) {Horse h = new BlackHorse();h.eat();
}

}
复制代码
  变量h的静态类型是Horse,而真实类型是BlackHorse。如果上面最后一行的eat()方法调用的是BlackHorse类的eat()方法,那么上面打印的就是“黑马吃草”;相反,如果上面的eat()方法调用的是Horse类的eat()方法,那么打印的就是“马吃草”。

所以,问题的核心就是Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。这样一来,上面最后一行的eat()方法调用的是BlackHorse类的eat()方法,打印的是“黑马吃草”。

二、分派的类型

一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参数统称做方法的宗量。比如下面例子中的Test类

复制代码
public class Test {

public void print(String str){System.out.println(str);
}

}
复制代码
  在上面的类中,print()方法属于Test对象,所以它的接收者也就是Test对象了。print()方法有一个参数是str,它的类型是String。

根据分派可以基于多少种宗量,可以将面向对象的语言划分为单分派语言(Uni-Dispatch)和多分派语言(Multi-Dispatch)。单分派语言根据一个宗量的类型进行对方法的选择,多分派语言根据多于一个的宗量的类型对方法进行选择。

C++和Java均是单分派语言,多分派语言的例子包括CLOS和Cecil。按照这样的区分,Java就是动态的单分派语言,因为这种语言的动态分派仅仅会考虑到方法的接收者的类型,同时又是静态的多分派语言,因为这种语言对重载方法的分派会考虑到方法的接收者的类型以及方法的所有参数的类型。

在一个支持动态单分派的语言里面,有两个条件决定了一个请求会调用哪一个操作:一是请求的名字,而是接收者的真实类型。单分派限制了方法的选择过程,使得只有一个宗量可以被考虑到,这个宗量通常就是方法的接收者。在Java语言里面,如果一个操作是作用于某个类型不明的对象上面,那么对这个对象的真实类型测试仅会发生一次,这就是动态的单分派的特征。

双重分派
  一个方法根据两个宗量的类型来决定执行不同的代码,这就是“双重分派”。Java语言不支持动态的多分派,也就意味着Java不支持动态的双分派。但是通过使用设计模式,也可以在Java语言里实现动态的双重分派。

在Java中可以通过两次方法调用来达到两次分派的目的。类图如下所示:
在这里插入图片描述
在图中有两个对象,左边的叫做West,右边的叫做East。现在West对象首先调用East对象的goEast()方法,并将它自己传入。在East对象被调用时,立即根据传入的参数知道了调用者是谁,于是反过来调用“调用者”对象的goWest()方法。通过两次调用将程序控制权轮番交给两个对象,其时序图如下所示:
在这里插入图片描述
这样就出现了两次方法调用,程序控制权被两个对象像传球一样,首先由West对象传给了East对象,然后又被返传给了West对象。

但是仅仅返传了一下球,并不能解决双重分派的问题。关键是怎样利用这两次调用,以及Java语言的动态单分派功能,使得在这种传球的过程中,能够触发两次单分派。

动态单分派在Java语言中是在子类重写父类的方法时发生的。换言之,West和East都必须分别置身于自己的类型等级结构中,如下图所示:
在这里插入图片描述
源代码
  West类

复制代码
public abstract class West {

public abstract void goWest1(SubEast1 east);public abstract void goWest2(SubEast2 east);

}
复制代码
  SubWest1类

复制代码
public class SubWest1 extends West{

@Override
public void goWest1(SubEast1 east) {System.out.println("SubWest1 + " + east.myName1());
}@Override
public void goWest2(SubEast2 east) {System.out.println("SubWest1 + " + east.myName2());
}

}
复制代码
  SubWest2类

复制代码
public class SubWest2 extends West{
@Override
public void goWest1(SubEast1 east) {

    System.out.println("SubWest2 + " + east.myName1());
}@Override
public void goWest2(SubEast2 east) {System.out.println("SubWest2 + " + east.myName2());
}

}
复制代码
  East类

public abstract class East {

public abstract void goEast(West west);

}
  SubEast1类

复制代码
public class SubEast1 extends East{
@Override
public void goEast(West west) {
west.goWest1(this);
}

public String myName1(){return "SubEast1";
}

}
复制代码
  SubEast2类

复制代码
public class SubEast2 extends East{
@Override
public void goEast(West west) {
west.goWest2(this);
}

public String myName2(){return "SubEast2";
}

}
复制代码
  客户端类

复制代码
public class Client {

public static void main(String[] args) {//组合1East east = new SubEast1();West west = new SubWest1();east.goEast(west);//组合2east = new SubEast1();west = new SubWest2();east.goEast(west);
}

}
复制代码
  运行结果如下

SubWest1 + SubEast1
SubWest2 + SubEast1

系统运行时,会首先创建SubWest1和SubEast1对象,然后客户端调用SubEast1的goEast()方法,并将SubWest1对象传入。由于SubEast1对象重写了其超类East的goEast()方法,因此,这个时候就发生了一次动态的单分派。当SubEast1对象接到调用时,会从参数中得到SubWest1对象,所以它就立即调用这个对象的goWest1()方法,并将自己传入。由于SubEast1对象有权选择调用哪一个对象,因此,在此时又进行一次动态的方法分派。

这个时候SubWest1对象就得到了SubEast1对象。通过调用这个对象myName1()方法,就可以打印出自己的名字和SubEast对象的名字,其时序图如下所示:
  在这里插入图片描述
由于这两个名字一个来自East等级结构,另一个来自West等级结构中,因此,它们的组合式是动态决定的。这就是动态双重分派的实现机制。

三、访问者模式的结构

访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。访问者模式的简略图如下所示:
在这里插入图片描述 数据结构的每一个节点都可以接受一个访问者的调用,此节点向访问者对象传入节点对象,而访问者对象则反过来执行节点对象的操作。这样的过程叫做“双重分派”。节点调用访问者,将它自己传入,访问者则将某算法针对此节点执行。访问者模式的示意性类图如下所示:
在这里插入图片描述
访问者模式涉及到的角色如下:

●  抽象访问者(Visitor)角色:声明了一个或者多个方法操作,形成所有的具体访问者角色必须实现的接口。

●  具体访问者(ConcreteVisitor)角色:实现抽象访问者所声明的接口,也就是抽象访问者所声明的各个访问操作。

●  抽象节点(Node)角色:声明一个接受操作,接受一个访问者对象作为一个参数。

●  具体节点(ConcreteNode)角色:实现了抽象节点所规定的接受操作。

●  结构对象(ObjectStructure)角色:有如下的责任,可以遍历结构中的所有元素;如果需要,提供一个高层次的接口让访问者对象可以访问每一个元素;如果需要,可以设计成一个复合对象或者一个聚集,如List或Set。

源代码
  可以看到,抽象访问者角色为每一个具体节点都准备了一个访问操作。由于有两个节点,因此,对应就有两个访问操作。

复制代码
public interface Visitor {
/**
* 对应于NodeA的访问操作
/
public void visit(NodeA node);
/
*
* 对应于NodeB的访问操作
*/
public void visit(NodeB node);
}
复制代码
  具体访问者VisitorA类

复制代码
public class VisitorA implements Visitor {
/**
* 对应于NodeA的访问操作
/
@Override
public void visit(NodeA node) {
System.out.println(node.operationA());
}
/
*
* 对应于NodeB的访问操作
*/
@Override
public void visit(NodeB node) {
System.out.println(node.operationB());
}

}
复制代码
  具体访问者VisitorB类

复制代码
public class VisitorB implements Visitor {
/**
* 对应于NodeA的访问操作
/
@Override
public void visit(NodeA node) {
System.out.println(node.operationA());
}
/
*
* 对应于NodeB的访问操作
*/
@Override
public void visit(NodeB node) {
System.out.println(node.operationB());
}

}
复制代码
  抽象节点类

复制代码
public abstract class Node {
/**
* 接受操作
*/
public abstract void accept(Visitor visitor);
}
复制代码
  具体节点类NodeA

复制代码
public class NodeA extends Node{
/**
* 接受操作
/
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
/
*
* NodeA特有的方法
*/
public String operationA(){
return “NodeA”;
}

}
复制代码
  具体节点类NodeB

复制代码
public class NodeB extends Node{
/**
* 接受方法
/
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
/
*
* NodeB特有的方法
*/
public String operationB(){
return “NodeB”;
}
}
复制代码
  结构对象角色类,这个结构对象角色持有一个聚集,并向外界提供add()方法作为对聚集的管理操作。通过调用这个方法,可以动态地增加一个新的节点。

复制代码
public class ObjectStructure {

private List<Node> nodes = new ArrayList<Node>();/*** 执行方法操作*/
public void action(Visitor visitor){for(Node node : nodes){node.accept(visitor);}}
/*** 添加一个新元素*/
public void add(Node node){nodes.add(node);
}

}
复制代码
  客户端类

复制代码
public class Client {

public static void main(String[] args) {//创建一个结构对象ObjectStructure os = new ObjectStructure();//给结构增加一个节点os.add(new NodeA());//给结构增加一个节点os.add(new NodeB());//创建一个访问者Visitor visitor = new VisitorA();os.action(visitor);
}

}
复制代码
  虽然在这个示意性的实现里并没有出现一个复杂的具有多个树枝节点的对象树结构,但是,在实际系统中访问者模式通常是用来处理复杂的对象树结构的,而且访问者模式可以用来处理跨越多个等级结构的树结构问题。这正是访问者模式的功能强大之处。

准备过程时序图
  首先,这个示意性的客户端创建了一个结构对象,然后将一个新的NodeA对象和一个新的NodeB对象传入。

其次,客户端创建了一个VisitorA对象,并将此对象传给结构对象。

然后,客户端调用结构对象聚集管理方法,将NodeA和NodeB节点加入到结构对象中去。

最后,客户端调用结构对象的行动方法action(),启动访问过程。
在这里插入图片描述  访问过程时序图
在这里插入图片描述
 结构对象会遍历它自己所保存的聚集中的所有节点,在本系统中就是节点NodeA和NodeB。首先NodeA会被访问到,这个访问是由以下的操作组成的:

(1)NodeA对象的接受方法accept()被调用,并将VisitorA对象本身传入;

(2)NodeA对象反过来调用VisitorA对象的访问方法,并将NodeA对象本身传入;

(3)VisitorA对象调用NodeA对象的特有方法operationA()。

从而就完成了双重分派过程,接着,NodeB会被访问,这个访问的过程和NodeA被访问的过程是一样的,这里不再叙述。

四、访问者模式的优点

●  好的扩展性

能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

●  好的复用性

可以通过访问者来定义整个对象结构通用的功能,从而提高复用程度。

●  分离无关行为

可以通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。

五、访问者模式的缺点

●  对象结构变化很困难

不适用于对象结构中的类经常变化的情况,因为对象结构发生了改变,访问者的接口和访问者的实现都要发生相应的改变,代价太高。

●  破坏封装

访问者模式通常需要对象结构开放内部数据给访问者和ObjectStructrue,这破坏了对象的封装性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/721312.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ACWing: 730.机器人跳跃问题

二分、递推 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N 1e5 10; int h[N] {0};int n,maxh0; // 当 E > maxh 时一定可以满足bool check(int mid){int E mid;for(int i 1;i < n;i){E 2 *…

电商直播大屏是什么?想搞这个怎么做?

随着电商行业的快速发展&#xff0c;直播带货已成为当下最热门的市场营销方式之一。为了更好地掌握直播数据&#xff0c;为企业决策提供有力支持&#xff0c;电商直播数据大屏应运而生。 一、电商直播数据大屏概述 电商直播数据大屏是一种集成了多种数据源的大屏幕可视化展示…

【管理咨询宝藏资料33】某头部咨询公司组织效能提升模型方案

本报告首发于公号“管理咨询宝藏”&#xff0c;如需阅读完整版报告内容&#xff0c;请查阅公号“管理咨询宝藏”。 【管理咨询宝藏资料33】某头部咨询公司组织效能提升模型方案 【关键词】战略规划、组织效能、管理咨询 【文件核心观点】 - 通过长期行业积累和市场洞察&#…

四电极测脂模块CSU18M91开发脂肪秤方案

一台脂肪秤通过测试体重、体脂、BMI、水分等数据并给出相应提示&#xff0c;并且许多人都将体脂检测数据作为身体健康指数衡量标准&#xff0c;辅助用户来关注身体健康&#xff0c;同时可以通过蓝牙与手机APP应用相连&#xff0c;记录日常身体变化情况&#xff0c;根据变化情况…

表单验证、属性绑定(一个属性根据另一个属性有无进行操作)

表单验证 一个属性根据另一个属性有无进行操作&#xff08;属性绑定&#xff09; 1、问题描述 ​ 需求&#xff1a;表单里面后两个属性需要根据前面一个属性进行有无判断。如果前面属性没有输入值&#xff0c;则不需要进行操作&#xff1b;如果前面属性有输入值&#xff0c;则…

通过scp在两台设备之间传输文件的sh脚本

前言 因项目需要&#xff0c;需要在两台设备发送大的文件&#xff0c;使用了Libcurl和libssh 和 libsftp等库&#xff0c;发现各种文件&#xff0c;连官方的demo都有文件&#xff0c;于是想到干脆写脚本还简单一些。 环境安装&#xff1a; sudo apt-get install expect 发送&a…

Windows如何安装docker-desktop

下载 docker-desktop设置环境安装wsl可能遇到的错误 下载 docker-desktop 下载官网&#xff1a;https://www.docker.com/products/docker-desktop/ 设置环境 如果没有Hyper-V选项的,按照以下步骤 添加一个文件Hyper-V.bat 添加以下内容,并双击运行后重启电脑 pushd "%~…

喜讯!聚铭网络实力入选「网安新兴赛道厂商速查指南」11大细分赛道

近日&#xff0c;国内首家专业聚焦网络安全商业市场研究分析和加速服务的机构——斯元商业咨询正式发布2024「网安新兴赛道厂商速查指南Emerging Technology Vendor Index」。 聚铭网络凭借在网络安全领域的深厚技术积累、丰富的行业应用经验和良好的客户口碑&#xff0c;成功…

大唐杯学习笔记:Day4

1.1NR帧结构 5G NR中,依然采用一帧10ms,并将一帧分为10子帧,每个子帧为1ms。每个子帧包含几个时隙(slot),每个时隙由14个OFDM符号构成(在常规CP下)。 μ \mu μ Δ f 2 μ ∗ 15 [ K H Z ] \Delta f2^{\mu}*15[KHZ] Δf2μ∗15[KHZ]Cyclic prefix015Normal130Normal260Normal…

运维随录实战(2)之k8s部署应用

一, 创建.gitlab-ci.yml文件 架构流程 文件内容 stages: #设置流水线模版- build # 编译- source2img- deploy # 发布variables: # 设置全局变量MAVEN_PATH: .m2MAVEM_IMAGE: maven:3.8.5-openjdk-17-slim # maven 打包使用的镜像MAVEN_CLI_OPTS: "-s $MAVEN_PATH/set…

红帆OA 多处 SQL注入漏洞复现

0x01 产品简介 红帆iOffice.net从最早满足医院行政办公需求(传统OA),到目前融合了卫生主管部门的管理规范和众多行业特色应用,是目前唯一定位于解决医院综合业务管理的软件,是最符合医院行业特点的医院综合业务管理平台,是成功案例最多的医院综合业务管理软件。 0x02 漏…

java中开源json处理库介绍

在Java生态系统中&#xff0c;有几个常用的开源库用于处理JSON数据。这些库各有特点&#xff0c;适用于不同的场景。下面我将介绍几个流行的Java JSON处理库&#xff0c;包括它们的基本情况、主要特点和核心API。 Jackson 基本情况 Jackson是一个流行的Java库&…

【李沐论文精读】Transformer精读

论文&#xff1a;Attention is All You Need 参考&#xff1a;李沐视频【Transformer论文逐段精读】、Transformer论文逐段精读【论文精读】、李沐视频精读系列 一、摘要 主流的序列转换(sequence transduction)模型都是基于复杂的循环或卷积神经网络&#xff0c;这个模型包含一…

java实现文件上传到本地

很多时候我们都需要进行文件上传和下载的操作&#xff0c;具体怎么实现网上的代码其实也是挺多的&#xff0c;刚好我的项目中也遇到了文件上传和下载的需求&#xff0c;本篇博文具体讲解上传操作&#xff0c;下篇博文讲解下载操作。 我们具体来想一想要将一个从前端传来的文件…

Servlet快速入门

什么是Servlet? Servlet可以用很多方式来描述&#xff0c;这取决于上下文。 是一种技术&#xff0c;它被用来创建一个Web应用程序。 是一个API&#xff0c;它提供了许多接口和类&#xff0c;包括文档。 是一个接口&#xff0c;创建任何Serwlet都必须实现这个接口。 是一个扩展…

Python影像变化监测-跟踪大盐湖的萎缩

使用 Google Colab 中的 Python 分析 Landsat-8 图像(2014-2023)以创建大盐湖表面区域的时间序列 目录 🌅大盐湖萎缩问题简介💾下载 Landsat-8 图像📈统计文件中的大盐湖地区时间序列⚙️处理 Landsat-8 图像🗺️大盐湖图像的可视化🎥 大盐湖缩小的延时摄影📉大盐…

通过联合部署DDoS高防和WAF提升网站防护能力

如果您的网站遭受的攻击既有流量型攻击&#xff0c;又混杂精巧的Web应用层攻击时&#xff08;例如SQL注入、跨站脚本攻击、命令注入等&#xff09;时&#xff0c;推荐您组合使用阿里云DDoS高防和Web 应用防火墙 WAF&#xff08;Web Application Firewall&#xff09;&#xff0…

C语言文件操作,linux文件操作,文件描述符,linux下一切皆文件,缓冲区,重定向

目录 C语言文件操作 如何打开文件以及打开文件方式 读写文件 关闭文件 Linux系统下的文件操作 open 宏标志位 write&#xff0c;read&#xff0c;close&#xff0c;lseek接口 什么是当前路径&#xff1f; linux下一切皆文件 文件描述符 文件描述符排序 C语言文件操…

【音视频开发好书推荐1】《RTC程序设计:实时音视频权威指南》

1、WebRTC概述 WebRTC&#xff08;Web Real-Time Communication&#xff09;是一个由Google发起的实时音视频通讯C开源库&#xff0c;其提供了音视频采集、编码、网络传输&#xff0c;解码显示等一整套音视频解决方案&#xff0c;我们可以通过该开源库快速地构建出一个音视频通…

牛客练习赛122

D:圆 正着求删除的最小代价不好做&#xff0c;采用逆向思维&#xff0c;求选择一些不相交的线段使得构成一个圆的代价尽量大&#xff0c;最后答案就是所有线段权值之和减去最大代价。 那么如何求这个最大代价呢&#xff1f;显然区间DP 老套路&#xff1a;破环成链&#xff0…