Java 设计模式——访问者模式
目录
- 1.概述
- 2.结构
- 3.案例实现
- 3.1.抽象访问者类
- 3.2.抽象元素类
- 3.3.具体元素类
- 3.4.具体访问者类
- 3.5.对象结构类
- 3.6.测试
- 4.优缺点
- 5.使用场景
- 6.扩展
- 6.1.分派
- 6.2.动态分配
- 6.3.静态分配
- 6.4.双分派
1.概述
访问者模式 (Visitor Pattern) 是一种行为型设计模式,它用于将数据结构和在数据结构上的操作分离开来。访问者模式可以让你在不修改数据结构的情况下,定义新的操作。
2.结构
访问者模式包含以下主要角色:
- 抽象访问者 (Visitor) 角色:定义了对每一个元素 (Element) 访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
- 具体访问者 (ConcreteVisitor) 角色:给出对每一个元素类访问时所产生的具体行为。
- 抽象元素 (Element) 角色: 定义了一个接受访问者的方法 (accept),其意义是指,每一个元素都要可以被访问者访问。
- 具体元素 (ConcreteElement) 角色:提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
- 对象结构 (Object Structure) 角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素 (Element),并且可以迭代这些元素,供访问者访问。
3.案例实现
【例】给宠物喂食:现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。类图如下:
具体实现代码如下:
3.1.抽象访问者类
Person.java
//抽象访问者角色接口
public interface Person {//给宠物猫喂食void feed(Cat cat);//给宠物狗喂食void feed(Dog dog);
}
3.2.抽象元素类
Animal.java
//抽象元素角色类
public interface Animal {//接受访问者访问的功能void accept(Person person);
}
3.3.具体元素类
Cat.java
//具体元素角色类(宠物猫)
public class Cat implements Animal{@Overridepublic void accept(Person person) {//访问者给宠物猫喂食person.feed(this);System.out.println("宠物猫接受喂食");}
}
Dog.java
//具体元素角色类(宠物狗)
public class Dog implements Animal{@Overridepublic void accept(Person person) {//访问者给宠物狗喂食person.feed(this);System.out.println("宠物狗接受喂食");}
}
3.4.具体访问者类
Owner.java
//具体访问者角色类(宠物主人)
public class Owner implements Person{@Overridepublic void feed(Cat cat) {System.out.println("主人给猫喂食");}@Overridepublic void feed(Dog dog) {System.out.println("主人给狗喂食");}
}
SomeOne.java
//具体访问者角色类(其他人)
public class SomeOne implements Person{@Overridepublic void feed(Cat cat) {System.out.println("其他人给猫喂食");}@Overridepublic void feed(Dog dog) {System.out.println("其他人给猫喂食");}
}
3.5.对象结构类
Home.java()
//对象结构类
public class Home {//声明一个集合对象,用来存储元素对象private List<Animal> nodeList = new ArrayList<>();//添加元素public void add(Animal animal){nodeList.add(animal);}public void action(Person person){//遍历集合,获取每一个元素,让访问者访问每一个元素for (Animal animal : nodeList) {animal.accept(person);}}
}
3.6.测试
Client.java
public class Client {public static void main(String[] args) {//创建 Home 对象Home home = new Home();//添加元素到 Home 对象中home.add(new Dog());home.add(new Cat());//创建主人对象Owner owner = new Owner();//让主人喂食所有的宠物home.action(owner);System.out.println("===============");//创建其他人对象SomeOne someOne = new SomeOne();//让其他人喂食所有的宠物home.action(someOne);}
}
结果如下:
主人给狗喂食
宠物狗接受喂食
主人给猫喂食
宠物猫接受喂食
===============
其他人给猫喂食
宠物狗接受喂食
其他人给猫喂食
宠物猫接受喂食
4.优缺点
(1)访问者模式的优点和缺点如下:
- 优点:
- 通过访问者模式,可以在不改变元素类的前提下,增加新的访问操作,从而扩展元素类的功能,符合开闭原则。
- 将数据结构和操作解耦,可以使得操作随着元素类的变化而变化,而不需要修改元素类,提高代码的可扩展性和可维护性。
- 在访问者模式中,访问者可以在访问元素的同时进行某些其他操作,这些其他操作可以是访问者所独有的,从而增加灵活性和适应性。
- 缺点:
- 访问者模式增加了类的数量,引入了新的接口和抽象类,增加了系统的复杂度。
- 对于不同类型的元素,如果新增了访问操作,则访问者的接口和实现都需要修改,增加了维护难度。
(2)综上,访问者模式适合在访问操作的种类比较固定的情况下使用,同时访问者的使用场景也是比较局限的,需要根据具体的场景来判断是否使用。
5.使用场景
(1)访问者模式适用于以下场景:
- 数据结构相对稳定,但需要定义新的操作:当数据结构的类层次结构相对稳定,但需要增加新的操作时,可以使用访问者模式。通过引入访问者模式,可以在不修改数据结构的情况下,定义新的操作。
- 数据结构和操作分离:当一个数据结构中的元素类比较固定,但需要对这些元素进行不同的操作时,可以使用访问者模式。通过将元素类和具体操作分离,可以增加灵活性和可扩展性。
- 数据结构中的元素稳定,但元素操作多变:当一个数据结构中的元素类相对稳定,但需要对这些元素进行多种操作时,可以使用访问者模式。通过将元素类和操作解耦,可以减少元素类的修改,提高代码的可维护性和可扩展性。
- 对数据结构的访问需求固定:当对一个数据结构的访问需求相对固定,但访问方式可以变化时,可以使用访问者模式。通过定义不同的访问者,可以对数据结构的不同部分进行不同的访问操作。
(2)需要注意的是,访问者模式的使用需要权衡代码的复杂性和可维护性,因此在选择使用访问者模式时,需要根据具体的需求和场景来判断是否合适。
6.扩展
事实上,访问者模式用到了一种名为双分派的技术。
6.1.分派
变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap()
,map 变量的静态类型是 Map,实际类型是 HashMap 。根据对象的类型而对方法进行的选择,就是分派 (Dispatch),分派又分为两种,即静态分派和动态分派。
- 静态分派 (Static Dispatch):发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。
- 动态分派 (Dynamic Dispatch):发生在运行时期,动态分派动态地置换掉某个方法。Java 通过方法的重写支持动态分派。
6.2.动态分配
通过方法的重写支持动态分派。
public class Animal { public void execute() { System.out.println("Animal"); }
}public class Dog extends Animal { @Override public void execute() { System.out.println("dog"); }
}public class Cat extends Animal { @Override public void execute() { System.out.println("cat");}
}public class Client { public static void main(String[] args) { Animal a1 = new Dog(); a1.execute(); Animal a2 = new Cat(); a2.execute(); }
}
运行结果如下:
dog
cat
上面代码的结果大家应该很容易想到,这不就是多态吗!运行执行的是子类中的方法。Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
6.3.静态分配
通过方法重载支持静态分派。
public class Animal {
}public class Dog extends Animal {
}public class Cat extends Animal {
}public class Execute { public void execute(Animal a) { System.out.println("Animal"); }public void execute(Dog d) { System.out.println("dog"); }public void execute(Cat c) { System.out.println("cat"); }
}public class Client { public static void main(String[] args) { Animal a = new Animal(); Animal a1 = new Dog(); Animal a2 = new Cat(); Execute exe = new Execute(); exe.execute(a); exe.execute(a1); exe.execute(a2); }
}
运行结果如下:
animal
animal
animal
这个结果可能出乎一些人的意料了,为什么呢?因为重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。
6.4.双分派
所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者 (receiver) 的运行时区别,还要根据参数的运行时区别。
(1)双分派技术 (Double Dispatch) 是一种多态性的应用,它允许在运行时根据两个对象的类型来确定方法的调用:
- 在传统的单分派多态性中,方法的调用取决于消息接收者的类型。
- 在双分派技术中,方法的调用依赖于两个对象的类型,即消息接收者和方法的参数。
(2)具体来说,双分派技术通过多次派发来确定要执行的方法。首先,根据消息接收者的类型,选择适当的方法版本。然后,根据方法的参数类型,再次选择适当的方法版本。这种双重派发的方式使得程序能够灵活地根据多个对象的类型进行方法调用,从而实现更加动态和灵活的行为。
一个常见的应用场景是访问者模式,其中访问者对象根据元素对象和自身的类型来决定要执行的操作方法。通过双分派技术,可以在访问者模式中根据元素和访问者的具体类型来选择正确的访问方法。
(3)下面以图形绘制为例,展示双分派技术的应用过程:假设有一个图形类库,其中定义了图形类 Shape
和绘制器类 Drawer
,其中 Shape 类有几个子类 Circle、Rect 和 Triangle,Drawer 类有几个子类 ColorDrawer、GrayDrawer 和 RedDrawer。现在需要根据不同的图形和绘制器来绘制不同的图形。
使用传统的单分派多态性方式来实现,需要为每个图形类和绘制器类的组合定义对应的 draw 方法,这个方法的实现是以所有可能的组合为基础,实现的类将有很多重复的代码。而双分派技术使用双重派发来避免这些重复的代码,具体流程如下:
- 定义 Shape 类的
accept
方法,传入一个 Drawer 实例作为参数,其中 accept 方法依赖于具体的Shape
子类。
public abstract class Shape {public abstract void accept(Drawer drawer);
}public class Circle extends Shape {@Overridepublic void accept(Drawer drawer) {drawer.drawCircle(this);}
}public class Rect extends Shape {@Overridepublic void accept(Drawer drawer) {drawer.drawRect(this);}
}public class Triangle extends Shape {@Overridepublic void accept(Drawer drawer) {drawer.drawTriangle(this);}
}
- 定义 Drawer 类的 drawCircle、drawRect 和 drawTriangle 方法,这些方法依赖于具体的
Drawer
子类。
public abstract class Drawer {public abstract void drawCircle(Circle circle);public abstract void drawRect(Rect rect);public abstract void drawTriangle(Triangle triangle);
}public class ColorDrawer extends Drawer {@Overridepublic void drawCircle(Circle circle) {System.out.println("绘制一个彩色的圆形");}@Overridepublic void drawRect(Rect rect) {System.out.println("绘制一个彩色的矩形");}@Overridepublic void drawTriangle(Triangle triangle) {System.out.println("绘制一个彩色的三角形");}
}public class GrayDrawer extends Drawer {@Overridepublic void drawCircle(Circle circle) {System.out.println("绘制一个灰色的圆形");}@Overridepublic void drawRect(Rect rect) {System.out.println("绘制一个灰色的矩形");}@Overridepublic void drawTriangle(Triangle triangle) {System.out.println("绘制一个灰色的三角形");}
}public class RedDrawer extends Drawer {@Overridepublic void drawCircle(Circle circle) {System.out.println("绘制一个红色的圆形");}@Overridepublic void drawRect(Rect rect) {System.out.println("绘制一个红色的矩形");}@Overridepublic void drawTriangle(Triangle triangle) {System.out.println("绘制一个红色的三角形");}
}
- 在主程序中根据具体的 Shape 子类和 Drawer 子类来调用对应的方法。
public class Main {public static void main(String[] args) {Shape shape = new Circle();Drawer drawer = new ColorDrawer();shape.accept(drawer); //输出结果为: 绘制一个彩色的圆形}
}
在上述示例中,accept 方法根据具体的 Shape 子类,调用对应的 Drawer 子类中的方法,在 Drawer 类中,具体的 drawCircle、drawRect 和 drawTriangle 方法会根据具体的 Shape 子类调用正确的方法。这样,通过双重派发技术,可以在运行时根据 Shape 子类和 Drawer 子类的具体类型来确定要调用的方法,从而避免了大量的重复代码。
(4)说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。
相关文章:

Java 设计模式——访问者模式
目录 1.概述2.结构3.案例实现3.1.抽象访问者类3.2.抽象元素类3.3.具体元素类3.4.具体访问者类3.5.对象结构类3.6.测试 4.优缺点5.使用场景6.扩展6.1.分派6.2.动态分配6.3.静态分配6.4.双分派 1.概述 访问者模式 (Visitor Pattern) 是一种行为型设计模式,它用于将数…...
Mysql联合查询
Mysql联合查询 T1表结构(用户名,密码) useridusernamepassword1jackpwd12owenpwd2 T2表结构(用户名,密码) useridjifendengji12033506 第一:内联(inner join) 如果想把用户信息、积分、等…...

响应式婴幼儿早教启蒙网站模板源码
模板信息: 模板编号:6830 模板编码:UTF8 模板颜色:红色 模板分类:学校、教育、培训、科研 适合行业:培训机构类企业 模板介绍: 本模板自带eyoucms内核,无需再下载eyou系统…...

ChatGPT付费创作系统V2.4.9独立版 +WEB端+ H5端 + 小程序端系统测试安装教程
播资源提供的GPT付费体验系统最新版系统是一款基于ThinkPHP框架开发的AI问答小程序,是基于国外很火的ChatGPT进行开发的Ai智能问答小程序。当前全民热议ChatGPT,流量超级大,引流不要太简单!一键下单即可拥有自己的GPT!…...

react 修改less文件后保存,内存溢出,项目崩溃问题解决
一、完整报错 一个很老的react项目,因为没有package-lock.json版本锁,导致拉下来的时候,安装的依赖版本冲突,好不容易启动起来,修改less文件后只要一保存,项目就会崩溃,需要重启,报…...
Lib文件和netlist的关系,DDC文件和netlist的区别
今天来说一说两个基础的概念: 1:综合用的Lib文件和netlist网表的关系 在数字IC设计中,Lib和网表都是非常重要的文件,但它们的作用和用途有很大的区别。 Lib文件,也称为库文件,主要包含单元级的信息&…...

万宾科技智能井盖,实现对井盖的监测
随着人工智能和物联网技术的不断变化,各种适用于市政府提高管理能力和公共服务水平的高科技产品不断更新。在道路基础设施建设过程中,智能井盖传感器的出现时刻保护着城市地下生命线,而且可以对地下水道井盖进行实时的监测并完成数据上传等工…...

使用Python从零实现多分类SVM
前言 本文将首先简要概述支持向量机及其训练和推理方程,然后将其转换为代码以开发支持向量机模型。之后然后将其扩展成多分类的场景,并通过使用Sci-kit Learn测试我们的模型来结束。 SVM概述 支持向量机的目标是拟合获得最大边缘的超平面(两个类中最近…...

WPF ToggleButton 主题切换动画按钮
WPF ToggleButton 主题切换动画按钮 仿造最近看到的html中的一个效果,大致思路是文章这样,感觉还可以再雕琢一下。 代码如下 XAML: <UserControl x:Class"WPFSwitch.AnimationSwitch"xmlns"http://schemas.microsoft.com/winfx/200…...
centerOS下docker 搭建IotDB集群
一、准备3台机器,IP地址依次为IP1,IP2,IP3,找一个目录下建立文件夹如下: ./data/confignode ./logs/confignode ./data/datanode ./logs/datanode二、在当前目录下建立docker-compose.yml文件,3台都要 1、…...
Vue3-Composition-API-学习笔记
01.Setup函数的体验 App.vue <template><div><h2>当前计数:{{ counter }}</h2><button click"increment">1</button><button click"decrement">-1</button></div> </template>&…...

NSS [HUBUCTF 2022 新生赛]checkin
NSS [HUBUCTF 2022 新生赛]checkin 判断条件是if ($data_unserialize[username]$username&&$data_unserialize[password]$password),满足则给我们flag。正常思路来说,我们要使序列化传入的username和password等于代码中的两个同名变量࿰…...

免费小程序HTTPS证书
随着互联网的快速发展,小程序已经成为人们日常生活中不可或缺的一部分。然而,在小程序的开发和使用过程中,安全问题一直是开发者们关注的重点。其中,HTTPS 证书是保障小程序安全的重要工具之一。在这方面,免费的小程序…...

Linux arm64异常简介和系统调用过程
文章目录 一、异常简介1.1 Exception levels1.2 异常类型 二、系统调用简介2.1 SVC指令2.2 VBAR2.3 系统调用保存现场2.4 系统调用返回 三、Linux 内核分析参考资料 一、异常简介 在ARM64体系架构中,异常是处理器在执行指令时可能遇到的不寻常情况或事件。这些异常…...
我遇到的最蠢的bug,竟然是因为这个原因……
bug的背景 我是一个Python开发者,我最近在做一个数据分析的项目,需要用到pandas库,来处理和分析一些表格数据我的功能需求是,根据用户输入的一些条件,从一个大的数据表中筛选出符合条件的数据,并生成一个新…...

【Mysql】查询mysql的版本
目录 cmd命令查询 mysql -- help(命令) mysql -u root -p(命令) 数据库管理工具查询 select version(); cmd命令查询 mysql -- help(命令) mysql -u root -p(命令) 执行该命令并且输入数据库密码 数据库管理工具查询 selec…...

广州华锐互动:VR互动实训内容编辑器助力教育创新升级
随着科技的飞速发展,教育领域也正在经历一场深刻的变革。其中,虚拟现实(VR)技术为教学活动提供了前所未有的便利和可能性。在诸多的VR应用中,VR互动实训内容编辑器无疑是最具潜力和创新性的一种。广州华锐互动开发的这款编辑器以其独特的功能…...

2023最新版本 从零基础入门C++与QT(学习笔记) -1- C++输入与输出
🎏说在前面 🎈我预计是使用两个月的时间玩转C与QT 🎈所以这是一篇学习笔记 🎈根据学习的效率可能提前完成学习,加油!!! 输入(代码如下方代码块) 🎄分析一下构成 🎈…...

Linux:权限篇 (彻底理清权限逻辑!)
shell命令以及运行原理: Linux严格意义上说的是一个操作系统,我们称之为“核心(kernel)“ ,但我们一般用户,不能直接使用kernel。而是通过kernel的“外壳”程序,也就是所谓的shell,来…...

classification_report分类报告的含义
classification_report分类报告 基础知识混淆矩阵(Confusion Matrix)TP、TN、FP、FN精度(Precision)准确率(Accuracy)召回率(Recall)F1分数(F1-score) classi…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...

基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

自然语言处理——循环神经网络
自然语言处理——循环神经网络 循环神经网络应用到基于机器学习的自然语言处理任务序列到类别同步的序列到序列模式异步的序列到序列模式 参数学习和长程依赖问题基于门控的循环神经网络门控循环单元(GRU)长短期记忆神经网络(LSTM)…...

Spring数据访问模块设计
前面我们已经完成了IoC和web模块的设计,聪明的码友立马就知道了,该到数据访问模块了,要不就这俩玩个6啊,查库势在必行,至此,它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据(数据库、No…...
稳定币的深度剖析与展望
一、引言 在当今数字化浪潮席卷全球的时代,加密货币作为一种新兴的金融现象,正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而,加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下,稳定…...

Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...

网站指纹识别
网站指纹识别 网站的最基本组成:服务器(操作系统)、中间件(web容器)、脚本语言、数据厍 为什么要了解这些?举个例子:发现了一个文件读取漏洞,我们需要读/etc/passwd,如…...

【分享】推荐一些办公小工具
1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由:大部分的转换软件需要收费,要么功能不齐全,而开会员又用不了几次浪费钱,借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...

基于Springboot+Vue的办公管理系统
角色: 管理员、员工 技术: 后端: SpringBoot, Vue2, MySQL, Mybatis-Plus 前端: Vue2, Element-UI, Axios, Echarts, Vue-Router 核心功能: 该办公管理系统是一个综合性的企业内部管理平台,旨在提升企业运营效率和员工管理水…...

Python 实现 Web 静态服务器(HTTP 协议)
目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1)下载安装包2)配置环境变量3)安装镜像4)node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1)使用 http-server2)详解 …...