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)说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。

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

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

相关文章

springboot本地启动多个模块报错:Address already in use: JVM_Bind

目录 背景解决方法 背景 环境&#xff1a; jdk1.8 idea 2019.2.4idea本地启动多个模块联调时&#xff0c;提示报错&#xff1a; 错误: 代理抛出异常错误: java.rmi.server.ExportException: Port already in use: 9090; nested exception is: java.net.BindException: Addre…

vue+java实现语音转文字思路

思路&#xff1a; 前端录音生成wav文件后端去解析 技术&#xff1a; 后端&#xff1a; Vosk是一个离线开源语音识别工具。它可以识别16种语言&#xff0c;包括中文。 API接口&#xff0c;让您可以只用几行代码&#xff0c;即可迅速免费调用、体验功能。 目前支持 WAV声音文件…

基于8086家具门安全控制系统设计

**单片机设计介绍&#xff0c;基于8086家具门安全控制系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 # 8086家具门安全控制系统设计介绍 8086家具门安全控制系统是一种用于保护家具和保证室内安全的系统。该系统基于808…

小程序游戏对接广告收益微信小游戏抖音游戏软件

小程序游戏对接广告是一种常见的游戏开发模式&#xff0c;开发者可以通过在游戏中嵌入广告来获取收益。以下是一些与小程序游戏对接广告收益相关的关键信息&#xff1a; 小程序游戏广告平台选择&#xff1a; 选择适合你的小程序游戏的广告平台非常重要。不同的平台提供不同类型…

ubuntu18-recvfrom接收不到广播报文异常分析

目录 前言 一、UDP广播接收程序 二、异常原因分析 总结 前言 在ubuntu18.04系统中&#xff0c;编写udp接收程序发现接收不到广播报文&#xff0c;使用抓包工具tcpdump可以抓取到广播报文&#xff0c;在此对该现象分析解析如下文所示。 一、UDP广播接收程序 UDP广播接收程序如…

【解决方案】vue 项目 npm run dev 时报错:‘cross-env‘ 不是内部或外部命令,也不是可运行的程序

报错 cross-env 不是内部或外部命令&#xff0c;也不是可运行的程序 或批处理文件。 npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! estate1.0.0 dev: cross-env webpack-dev-server --inline --progress --config build/webpack.dev.conf.js npm ERR! Exit status 1 np…

Go语言用Colly库编写的图像爬虫程序

下面是一个使用Colly库编写的Go语言图像爬虫程序&#xff0c;该程序会爬取news.qq上的图片&#xff0c;并使用proxy_host:duoip和proxy_port:8000的爬虫IP服务器进行抓取。 package mainimport ("fmt""net/http""github.com/crawlab-collective/go-co…

21 移动网络的前世今生

1、移动网络的发展历程 发展过程就是&#xff1a;2G,3G,4G,5G的过程&#xff0c;用2G看txt&#xff0c;用3G看jpg&#xff0c;用4G看avi。 2、2G网络 手机本来是用来打电话的&#xff0c;不是用来上网的&#xff0c;所以原来在2G时代&#xff0c;上网使用的不是IP网络&#…

论文阅读——What Can Human Sketches Do for Object Detection?(cvpr2023)

论文&#xff1a;https://openaccess.thecvf.com/content/CVPR2023/papers/Chowdhury_What_Can_Human_Sketches_Do_for_Object_Detection_CVPR_2023_paper.pdf 代码&#xff1a;What Can Human Sketches Do for Object Detection? (pinakinathc.me) 一、 Baseline SBIR Fram…

【Qt之QAssociativeIterable】使用

介绍 QAssociativeIterable类是QVariant中一个关联式容器的可迭代接口。这个类允许多种访问在QVariant中保存的关联式容器元素的方法。如果一个QVariant可以转换为QVariantHash或QVariantMap&#xff0c;那么QAssociativeIterable的实例可以从中提取出来。 QHash<int, QSt…

UPLOAD-LABS1

less1 (js验证) 我们上传PHP的发现不可以&#xff0c;只能是jpg&#xff0c;png&#xff0c;gif&#xff08;白名单限制了&#xff09; 我们可以直接去修改限制 在查看器中看到使用了onsubmit这个函数&#xff0c;触发了鼠标的单击事件&#xff0c;在表单提交后马上调用了re…

css排版—— 一篇优雅的文章(中英文) vs 聊天框的特别排版

文章 <div class"contentBox"><p>这是一篇范文——仅供测试使用</p><p>With the coming of national day, I have a one week holiday. I reallyexpect to it, because it want to have a short trip during these days. Iwill travel to Ji…

osgEarth之添加shp

目录 效果 代码 代码分析 加载模式 效果 代码 #include "stdafx.h" #include <osg/Notify> #include <osgGA/StateSetManipulator> #include <osgViewer/Viewer> #include <osgViewer/ViewerEventHandlers>#include <osgEarth/MapNo…

蓝桥杯练习

即约分数 题目 思路 遍历所有的x&#xff0c;y&#xff0c;判断x/y是不是即越约分数。 代码 #include <iostream> using namespace std; int gcd(int x,int y) {int r;while(y!0){rx%y;xy;yr;}return x; } int main() {// 请在此输入您的代码int sum4039;//1/y和x/1都…

火爆全网!用 Pyecharts 就能做出来“迁徙图“和“轮播图“

1.pyecharts知识点回顾 1&#xff09;知识回顾 前面我们已经讲述了&#xff0c;如何使用pyecharts进行图形的绘制&#xff0c;一共涉及到如下四步。我们今天就是按照下面这几步来进行迁徙图和轮播图的绘制。 ① 选择图表类型&#xff1b; ② 声明图形类并添加数据&#xff1…

学术论文的实证数据来源

一、引言 在当今的学术研究中&#xff0c;数据是至关重要的。无论是自然科学、社会科学还是人文科学&#xff0c;都需要借助数据来支撑和证明其研究假设和理论。然而&#xff0c;数据的来源却是多种多样的&#xff0c;而且不同的学科领域也有其特定的数据来源。本文旨在探讨论文…

开放智慧,助力学习——电大搜题,打开学无止境的新篇章

随着信息技术的迅猛发展&#xff0c;学习已经不再受时间和空间的限制。电大搜题微信公众号为广播电视大学和河南开放大学的学子们带来了便利和智慧&#xff0c;让学习变得更加高效和愉快。 电大搜题微信公众号作为一款专为电大学生而设计的学习助手&#xff0c;是学习中不可或…

【2】Spring Boot 3 项目搭建

目录 【2】Spring Boot 3 初始项目搭建项目生成1. 使用IDEA商业版创建2. 使用官方start脚手架创建 配置与启动Git版本控制 个人主页: 【⭐️个人主页】 需要您的【&#x1f496; 点赞关注】支持 &#x1f4af; 【2】Spring Boot 3 初始项目搭建 项目生成 1. 使用IDEA商业版创…

ppt聚光灯效果

1.放入三张图片内容或其他 2.全选复制成图片 3.设置黑色矩形&#xff0c;透明度30% 4.粘贴复制后的图片&#xff0c;制定图层 5.插入椭圆&#xff0c;先选中矩形&#xff0c;再选中椭圆&#xff0c;点击绘图工具&#xff0c;选择相交即可&#xff08;关键&#xff09;

全景房屋装修vr可视化编辑软件功能及特点

VR样板间、VR景观、VR商业街&#xff0c;全方位展示建筑内外空间使用及功能表現&#xff0c;让目标客戶能够身临其境体验項目的每处细节。 同时支持微信传播&#xff0c;线上看房&#xff0c;手机端VR沉浸式体验 3D互动售楼系统 3D互动售楼系统&#xff0c;集项目展示、智能选房…