Java 设计模式——访问者模式

news2024/11/25 2:27:57

目录

  • 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{
    @Override
    public void accept(Person person) {
        //访问者给宠物猫喂食
        person.feed(this);
        System.out.println("宠物猫接受喂食");
    }
}

Dog.java

//具体元素角色类(宠物狗)
public class Dog implements Animal{
    @Override
    public void accept(Person person) {
        //访问者给宠物狗喂食
        person.feed(this);
        System.out.println("宠物狗接受喂食");
    }
}

3.4.具体访问者类

Owner.java

//具体访问者角色类(宠物主人)
public class Owner implements Person{
    @Override
    public void feed(Cat cat) {
        System.out.println("主人给猫喂食");
    }
    
    @Override
    public void feed(Dog dog) {
        System.out.println("主人给狗喂食");
    }
}

SomeOne.java

//具体访问者角色类(其他人)
public class SomeOne implements Person{
    @Override
    public void feed(Cat cat) {
        System.out.println("其他人给猫喂食");
    }
    
    @Override
    public 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 {
    @Override
    public void accept(Drawer drawer) {
        drawer.drawCircle(this);
    }
}

public class Rect extends Shape {
    @Override
    public void accept(Drawer drawer) {
        drawer.drawRect(this);
    }
}

public class Triangle extends Shape {
    @Override
    public 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 {
    @Override
    public void drawCircle(Circle circle) {
        System.out.println("绘制一个彩色的圆形");
    }

    @Override
    public void drawRect(Rect rect) {
        System.out.println("绘制一个彩色的矩形");
    }

    @Override
    public void drawTriangle(Triangle triangle) {
        System.out.println("绘制一个彩色的三角形");
    }
}

public class GrayDrawer extends Drawer {
    @Override
    public void drawCircle(Circle circle) {
        System.out.println("绘制一个灰色的圆形");
    }

    @Override
    public void drawRect(Rect rect) {
        System.out.println("绘制一个灰色的矩形");
    }

    @Override
    public void drawTriangle(Triangle triangle) {
        System.out.println("绘制一个灰色的三角形");
    }
}

public class RedDrawer extends Drawer {
    @Override
    public void drawCircle(Circle circle) {
        System.out.println("绘制一个红色的圆形");
    }

    @Override
    public void drawRect(Rect rect) {
        System.out.println("绘制一个红色的矩形");
    }

    @Override
    public 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.coloradmin.cn/o/1191280.html

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

相关文章

潼南柠檬产业发展大会举行 这三个场景“柠”聚了人气

华龙网讯&#xff08;首席记者 羊华&#xff09;今&#xff08;6&#xff09;日下午&#xff0c;2023中国潼南柠檬产业发展大会正式举行。潼南区准备了“大礼包”&#xff0c;既有专业论坛峰会&#xff0c;也首发了“柠檬产业大脑”&#xff0c;还进行了招商引资集中签约&#…

Linux安装git和maven——拉取代码 --> mvn打包成jar包 --->运行jar包

前言 我们知道最后的代码都是要运行在Linux系统中的&#xff0c;所以就需要在Linux中安装git&#xff0c;从而能够拉取代码&#xff0c;安装maven&#xff0c;从而能够进行项目的打包。 本篇博客以centos为例&#xff0c;介绍如何安装git&#xff0c;安装maven3.8&#xff0c…

【LLMs】从大语言模型到表征再到知识图谱

从大语言模型到表征再到知识图谱 InstructGLMLLM如何学习拓扑&#xff1f;构建InstructGLM泛化InstructGLM补充参考资料 2023年8月14日&#xff0c;张永峰等人的论文《Natural Language is All a Graph Needs》登上arXiv街头&#xff0c;轰动一时&#xff01;本论文概述了一个名…

界面组件DevExpress ASP.NET Core v23.1 - 进一步升级UI组件

DevExpress ASP.NET Core Controls使用强大的混合方法&#xff0c;结合现代企业Web开发工具所期望的所有功能。该套件通过ASP.NET Razor标记和服务器端ASP.NET Core Web API的生产力和简便性&#xff0c;提供客户端JavaScript的性能和灵活性。ThemeBuilder工具和集成的Material…

为什么推荐从Linux开始了解IT技术

IT是什么&#xff0c;是干什么的呢&#xff1f; 说起物联网&#xff0c;云计算&#xff0c;大数据&#xff0c;或许大家听过。但是&#xff0c;你知道&#xff0c;像云计算的底层基座是什么呢&#xff1f;就是我们现在说的Linux操作系统。而云计算就是跑在Linux操作系统上的一个…

管理驾驶舱这么做,领导都点赞(附方案下载)

你是否知道你的企业是否充分利用了可用的数据资源&#xff1f; 著名的著名的质量管理专家&#xff0c;威廉爱德华德莱克&#xff08;William Edwards Deming&#xff09;曾说过&#xff1a;"数据不是权力&#xff0c;能够理解数据的能力才是真正的权力。" 企业在经营…

Unity Mirror学习(一) SyncVars属性使用

官网中所说的网络对象&#xff0c;指的是挂了 NetworkIdentity组件的对象 官网中所说的玩家对象&#xff0c;指的是NetworkManager脚本上的PlayerPrefab预制体 这个概念对阅读官网文档很重要&#xff0c;我刚开始并不理解&#xff0c;走了歪路 SyncVars&#xff08;同步变量&a…

ssm+vue的论文管理系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的论文管理系统(有报告)。Javaee项目&#xff0c;ssm vue前后端分离项目。 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 项目介绍&#xff1a; 采用M&am…

使用promise创建一个同步事件驱动api

使用promise创建一个同步事件驱动api 事件驱动体系结构(EDA)是一种强大的方法&#xff0c;可以在网络上构建松散耦合、性能好、可伸缩的应用程序。它支持非常多功能&#xff0c;如推送通知、协作编辑和多人操作&#xff0c;以及其他实时交互。 但有时模型与我们开发人员需要的…

工业镜头接口类型

现有产品主要有以下接口 1、C:最常见的工业相机接口&#xff0c;受限于接口物理尺寸大小&#xff0c;最大靶面目前是4/3” 2、M42:M421.0,2k和4k线阵相机使用 3、M58S:M580.75,大靶面相机使用&#xff0c;可以转C(限于CH080相机&#xff0c;靶面4/3”)&#xff0c;可以转F,可以…

Selenium关于内容信息的获取读取

在进行自然语言处理、文本分类聚类、推荐系统、舆情分析等研究中,通常需要使用新浪微博的数据作为语料,这篇文章主要介绍如果使用Python和Selenium爬取自定义新浪微博语料。因为网上完整的语料比较少,而使用Selenium方法有点简单、速度也比较慢,但方法可行,同时能够输入验…

中国第二批,11个大模型备案获批

加上首批的 10 余个大模型&#xff0c;目前已有超过 20 个大模型获得审批。 据钛媒体独家报道&#xff0c;国内第二批通过备案的AI大模型包括11家公司&#xff0c;部分已面向全社会开放服务。加上首批的10余个大模型&#xff0c;目前已有超过20个大模型获得备案。 新一批备案…

Python数据容器(列表)

目录 一.什么是数据容器二.数据容器&#xff1a;列表1.列表的定义2.列表的下标索引3.列表的常用操作4.总结5.练习 三.列表的遍历1.列表的遍历2.while循环和for循环的对比3.练习 一.什么是数据容器 1.什么是数据容器 一种可以存储多个元素的Python数据类型 2.Python有哪些数…

c语言总是有小问题,是练的少吗?

c语言总是有小问题&#xff0c;是练的少吗&#xff1f; 题主说我做c语言的题目时候&#xff0c;是有思路的并且可以按照想法写下来&#xff0c;大体上看没有问题&#xff0c;但是到运行的时候总是不过关。就需要很长的时间找出那个细微的错误&#xff0c;这种细微的错误怎么才能…

“深入理解机器学习性能评估指标:TP、TN、FP、FN、精确率、召回率、准确率、F1-score和mAP”

目录 引言 分类标准 示例&#xff1a;癌症检测 1. 精确率&#xff08;Precision&#xff09; 2. 召回率&#xff08;Recall&#xff09; 3. 准确率&#xff08;Accuracy&#xff09; 4. F1-score 5. mAP&#xff08;均值平均精度&#xff09; 总结与通俗解释 引言 机器…

自动驾驶算法(九):多项式轨迹与Minimun Snap原理与Matab代码详解

目录 1 为什么需要轨迹优化 2 代码解析 3 完整代码 1 为什么需要轨迹优化 我们利用前八篇所学的博客可以利用RRT、A*、遗传算法等设计出一条折线轨迹&#xff0c;轨迹优化就是在路径优化的基础上将折线优化成曲线&#xff0c;这样更加有利于无人机的飞行。 那么什么是多项式轨…

史上第一款AOSP开发的IDE (支持Java/Kotlin/C++/Jni/Native/Shell/Python)

ASFP Study 史上第一款AOSP开发的IDE &#xff08;支持Java/Kotlin/C/Jni/Native/Shell/Python&#xff09; 类似于Android Studio&#xff0c;可用于开发Android系统源码。 Android studio for platform&#xff0c;简称asfp(爱上富婆)。 背景&下载&使用 背景 由…

8.4 矢量图层点要素分类(Categorized)渲染使用

文章目录 前言分类&#xff08;Categorized&#xff09;渲染QGis代码实现 总结 前言 前面几章介绍了矢量-点要素-单一符号的各种用法所谓单一符号是指点要素的符号在图层显示时只有一种形式下面介绍的分类&#xff08;Categorized&#xff09;渲染说明&#xff1a;文章中的示例…

【Python爬虫库】pytube使用方法

一、pytube库简介 pytube库是一个python第三方库&#xff0c;用于youtube视频的抓取和其他相关操作。官方文档&#xff1a;pytube 二、基本操作 1、显示视频标题 from pytube import YouTube yt YouTube(https://youtube.com/watch?vIAJsZWhj6GI) print(yt.title)说明&am…

自建网盘平台搭建(源码+教程)

为什么要自己搭建网盘&#xff0c;现在许多大厂的网盘&#xff0c;文件都添加了许多限制&#xff0c;有好多文件会遭到和谐&#xff0c;而且大部分网盘也都会限速&#xff0c;不开通VIP是很难用的&#xff01;这是一套可以运营的网盘&#xff0c;代码无加密可以进行二次开发。下…