目录
1 使用访问者模式实现KPI考核的场景
2 从静态分派到动态分派
2.1 静态分派
2.2 动态分派
3 访问者模式中的伪动态分派
4 访问者模式在JDK源码中的应用
5 访问者模式在Spring源码中的应用
首页 Java java教程 Java访问者模式的静态、动态和伪动态分派及实例分析

Java访问者模式的静态、动态和伪动态分派及实例分析

Apr 26, 2023 pm 08:52 PM
java

1 使用访问者模式实现KPI考核的场景

每到年底,管理层就要开始评定员工一年的工作绩效,员工分为工程师和经理;管理层有CEO和CTO。那么CTO关注工程师的代码量、经理的新产品数量;CEO关注工程师的KPI、经理的KPI及新产品数量。

由于CEO和CTO对于不同的员工的关注点是不一样的,这就需要对不同的员工类型进行不同的处理。此时,访问者模式可以派上用场了,来看代码。

//员工基类
public abstract class Employee {
    public String name;
    public int kpi;//员工KPI
    public Employee(String name) {
        this.name = name;
        kpi = new Random().nextInt(10);
    }
    //核心方法,接受访问者的访问
    public abstract void accept(IVisitor visitor);
}
登录后复制

Employee类定义了员工基本信息及一个accept()方法,accept()方法表示接受访问者的访问,由具体的子类来实现。访问者是一个接口,传入不同的实现类,可访问不同的数据。下面看工程师Engineer类的代码。

//工程师
public class Engineer extends Employee {
    public Engineer(String name) {
        super(name);
    }
    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }
    //工程师一年的代码量
    public int getCodeLines() {
        return new Random().nextInt(10 * 10000);
    }
}
登录后复制

经理Manager类的代码如下。

//经理
public class Manager extends Employee {
    public Manager(String name) {
        super(name);
    }
    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }
    //一年做的新产品数量
    public int getProducts() {
        return new Random().nextInt(10);
    }
}
登录后复制

工程师被考核的是代码量,经理被考核的是新产品数量,二者的职责不一样。也正是因为有这样的差异性,才使得访问模式能够在这个场景下发挥作用。Employee、Engineer、Manager 3个类型相当于数据结构,这些类型相对稳定,不会发生变化。

将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的showReport()方法查看所有员工的业绩,代码如下。

//员工业务报表类
public class BusinessReport {
    private List<Employee> employees = new LinkedList<Employee>();
    public BusinessReport() {
        employees.add(new Manager("经理-A"));
        employees.add(new Engineer("工程师-A"));
        employees.add(new Engineer("工程师-B"));
        employees.add(new Engineer("工程师-C"));
        employees.add(new Manager("经理-B"));
        employees.add(new Engineer("工程师-D"));
    }
    /**
     * 为访问者展示报表
     * @param visitor 公司高层,如CEO、CTO
     */
    public void showReport(IVisitor visitor) {
        for (Employee employee : employees) {
            employee.accept(visitor);
        }
    }
}
登录后复制

下面来看访问者类型的定义,访问者声明了两个visit()方法,分别对工程师和经理访问,代码如下。

public interface IVisitor {
    //访问工程师类型
    void visit(Engineer engineer);
    //访问经理类型
    void visit(Manager manager);
}
登录后复制

上面代码定义了一个IVisitor接口,该接口有两个visit()方法,参数分别是Engineer和Manager,也就是说对于Engineer和Manager的访问会调用两个不同的方法,以此达到差异化处理的目的。这两个访问者具体的实现类为CEOVisitor类和CTOVisitor类。首先来看CEOVisitor类的代码。

//CEO访问者
public class CEOVisitor implements IVisitor {
    public void visit(Engineer engineer) {
        System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
    }
    public void visit(Manager manager) {
        System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
                ", 新产品数量: " + manager.getProducts());
    }
}
登录后复制

在CEO的访问者中,CEO关注工程师的KPI、经理的KPI和新产品数量,通过两个visit()方法分别进行处理。如果不使用访问者模式,只通过一个visit()方法进行处理,则需要在这个visit()方法中进行判断,然后分别处理,代码如下。

public class ReportUtil {
    public void visit(Employee employee) {
        if (employee instanceof Manager) {
            Manager manager = (Manager) employee;
            System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
                    ", 新产品数量: " + manager.getProducts());
        } else if (employee instanceof Engineer) {
            Engineer engineer = (Engineer) employee;
            System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
        }
    }
}
登录后复制

这就导致了if...else逻辑的嵌套及类型的强制转换,难以扩展和维护,当类型较多时,这个ReportUtil就会很复杂。而使用访问者模式,通过同一个函数对不同的元素类型进行相应处理,使结构更加清晰、灵活性更高。然后添加一个CTO的访问者类CTOVisitor。

public class CTOVisitor implements IVisitor {
    public void visit(Engineer engineer) {
        System.out.println("工程师: " + engineer.name + ", 代码行数: " + engineer.getCodeLines());
    }
    public void visit(Manager manager) {
        System.out.println("经理: " + manager.name + ", 产品数量: " + manager.getProducts());
    }
}
登录后复制

重载的visit()方法会对元素进行不同的操作,而通过注入不同的访问者又可以替换掉访问者的具体实现,使得对元素的操作变得更灵活,可扩展性更高,同时,消除了类型转换、if...else等“丑陋”的代码。

客户端测试代码如下。

public static void main(String[] args) {
        //构建报表
        BusinessReport report = new BusinessReport();
        System.out.println("=========== CEO看报表 ===========");
        report.showReport(new CEOVisitor());
        System.out.println("=========== CTO看报表 ===========");
        report.showReport(new CTOVisitor());
}
登录后复制

运行结果如下图所示。

java访问者模式的静态动态及伪动态分派实例分析

file

在上述案例中,Employee扮演了Element角色,Engineer和Manager都是 ConcreteElement,CEOVisitor和CTOVisitor都是具体的Visitor对象,BusinessReport就是ObjectStructure。

访问者模式最大的优点就是增加访问者非常容易,从代码中可以看到,如果要增加一个访问者,则只要新实现一个访问者接口的类,从而达到数据对象与数据操作相分离的效果。如果不使用访问者模式,而又不想对不同的元素进行不同的操作,则必定需要使用if...else和类型转换,这使得代码难以升级维护。

我们要根据具体情况来评估是否适合使用访问者模式。例如,对象结构是否足够稳定,是否需要经常定义新的操作,使用访问者模式是否能优化代码,而不使代码变得更复杂。

2 从静态分派到动态分派

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

List list = null;
list = new ArrayList();
登录后复制

上面代码声明了一个变量list,它的静态类型(也叫作明显类型)是List,而它的实际类型是ArrayList。根据对象的类型对方法进行的选择,就是分派(Dispatch)。分派又分为两种,即静态分派和动态分派。

2.1 静态分派

静态分派(Static Dispatch)就是按照变量的静态类型进行分派,从而确定方法的执行版本,静态分派在编译期就可以确定方法的版本。而静态分派最典型的应用就是方法重载,来看下面的代码。

public class Main {
    public void test(String string){
        System.out.println("string");
    }
    public void test(Integer integer){
        System.out.println("integer");
    }
    public static void main(String[] args) {
        String string = "1";
        Integer integer = 1;
        Main main = new Main();
        main.test(integer);
        main.test(string);
    }
}
登录后复制

在静态分派判断的时候,根据多个判断依据(即参数类型和个数)判断出方法的版本,这就是多分派的概念,因为我们有一个以上的考量标准,所以Java是静态多分派的语言。

2.2 动态分派

对于动态分派,与静态分派相反,它不是在编译期确定的方法版本,而是在运行时才能确定的。而动态分派最典型的应用就是多态的特性。举个例子,来看下面的代码。

interface Person{
    void test();
}
class Man implements Person{
    public void test(){
        System.out.println("男人");
    }
}
class Woman implements Person{
    public void test(){
        System.out.println("女人");
    }
}
public class Main {
    public static void main(String[] args) {
        Person man = new Man();
        Person woman = new Woman();
        man.test();
        woman.test();
    }
}
登录后复制

这段代码的输出结果为依次打印男人和女人,然而这里的test()方法版本,无法根据Man和Woman的静态类型判断,他们的静态类型都是Person接口,根本无从判断。

显然,产生这样的输出结果,就是因为test()方法的版本是在运行时判断的,这就是动态分派。

动态分派判断的方法是在运行时获取Man和Woman的实际引用类型,再确定方法的版本,而由于此时判断的依据只是实际引用类型,只有一个判断依据,所以这就是单分派的概念,这时考量标准只有一个,即变量的实际引用类型。相应地,这说明Java是动态单分派的语言。

3 访问者模式中的伪动态分派

通过前面的分析,我们知道Java是静态多分派、动态单分派的语言。Java底层不支持动态双分派。但是通过使用设计模式,也可以在Java里实现伪动态双分派。在访问者模式中使用的就是伪动态双分派。所谓动态双分派就是在运行时依据两个实际类型去判断一个方法的运行行为,而访问者模式实现的手段是进行两次动态单分派来达到这个效果。

还是回到前面的KPI考核业务场景中,BusinessReport类中的showReport()方法的代码如下。

public void showReport(IVisitor visitor) {
        for (Employee employee : employees) {
            employee.accept(visitor);
        }
}
登录后复制

这里依据Employee和IVisitor两个实际类型决定了showReport()方法的执行结果,从而决定了accept()方法的动作。

accept()方法的调用过程分析如下。

(1)当调用accept()方法时,根据Employee的实际类型决定是调用Engineer还是Manager的accept()方法。

(2)这时accept()方法的版本已经确定,假如是Engineer,则它的accept()方法调用下面这行代码。

    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }		
登录后复制

此时的this是Engineer类型,因此对应的是IVisitor接口的visit(Engineer engineer)方法,此时需要再根据访问者的实际类型确定visit()方法的版本,如此一来,就完成了动态双分派的过程。

以上过程通过两次动态双分派,第一次对accept()方法进行动态分派,第二次对访问者的visit()方法进行动态分派,从而达到根据两个实际类型确定一个方法的行为的效果。

而原本的做法通常是传入一个接口,直接使用该接口的方法,此为动态单分派,就像策略模式一样。在这里,showReport()方法传入的访问者接口并不是直接调用自己的visit()方法,而是通过Employee的实际类型先动态分派一次,然后在分派后确定的方法版本里进行自己的动态分派。

注:这里确定accept(IVisitor visitor)方法是由静态分派决定的,所以这个并不在此次动态双分派的范畴内,而且静态分派是在编译期完成的,所以accept(IVisitor visitor)方法的静态分派与访问者模式的动态双分派并没有任何关系。动态双分派说到底还是动态分派,是在运行时发生的,它与静态分派有着本质上的区别,不可以说一次动态分派加一次静态分派就是动态双分派,而且访问者模式的双分派本身也是另有所指。

而this的类型不是动态分派确定的,把它写在哪个类中,它的静态类型就是哪个类,这是在编译期就确定的,不确定的是它的实际类型,请小伙伴们也要区分开来。

4 访问者模式在JDK源码中的应用

首先来看JDK的NIO模块下的FileVisitor接口,它提供了递归遍历文件树的支持。这个接口上的方法表示了遍历过程中的关键过程,允许在文件被访问、目录将被访问、目录已被访问、发生错误等过程中进行控制。换句话说,这个接口在文件被访问前、访问中和访问后,以及产生错误的时候都有相应的钩子程序进行处理。

调用FileVisitor中的方法,会返回访问结果的FileVisitResult对象值,用于决定当前操作完成后接下来该如何处理。FileVisitResult的标准返回值存放在FileVisitResult枚举类型中,代码如下。

public interface FileVisitor<T> {
    FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
        throws IOException;
    FileVisitResult visitFile(T file, BasicFileAttributes attrs)
        throws IOException;
    FileVisitResult visitFileFailed(T file, IOException exc)
        throws IOException;
    FileVisitResult postVisitDirectory(T dir, IOException exc)
        throws IOException;
}
登录后复制

(1)FileVisitResult.CONTINUE:这个访问结果表示当前的遍历过程将会继续。

(2)FileVisitResult.SKIP_SIBLINGS:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前文件/目录的兄弟节点。

(3)FileVisitResult.SKIP_SUBTREE:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前目录下的所有节点。

(4)FileVisitResult.TERMINATE:这个访问结果表示当前的遍历过程将会停止。

通过访问者去遍历文件树会比较方便,比如查找文件夹内符合某个条件的文件或者某一天内所创建的文件,这个类中都提供了相对应的方法。它的实现其实也非常简单,代码如下。

public class SimpleFileVisitor<T> implements FileVisitor<T> {
    protected SimpleFileVisitor() {
    }
    @Override
    public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
        throws IOException
    {
        Objects.requireNonNull(dir);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }
    @Override
    public FileVisitResult visitFile(T file, BasicFileAttributes attrs)
        throws IOException
    {
        Objects.requireNonNull(file);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }
    @Override
    public FileVisitResult visitFileFailed(T file, IOException exc)
        throws IOException
    {
        Objects.requireNonNull(file);
        throw exc;
    }
    @Override
    public FileVisitResult postVisitDirectory(T dir, IOException exc)
        throws IOException
    {
        Objects.requireNonNull(dir);
        if (exc != null)
            throw exc;
        return FileVisitResult.CONTINUE;
    }
}
登录后复制

5 访问者模式在Spring源码中的应用

再来看访问者模式在Spring中的应用,Spring IoC中有个BeanDefinitionVisitor类,其中有一个visitBeanDefinition()方法,源码如下。

public class BeanDefinitionVisitor {
	@Nullable
	private StringValueResolver valueResolver;

	public BeanDefinitionVisitor(StringValueResolver valueResolver) {
		Assert.notNull(valueResolver, "StringValueResolver must not be null");
		this.valueResolver = valueResolver;
	}
	protected BeanDefinitionVisitor() {
	}
	public void visitBeanDefinition(BeanDefinition beanDefinition) {
		visitParentName(beanDefinition);
		visitBeanClassName(beanDefinition);
		visitFactoryBeanName(beanDefinition);
		visitFactoryMethodName(beanDefinition);
		visitScope(beanDefinition);
		if (beanDefinition.hasPropertyValues()) {
			visitPropertyValues(beanDefinition.getPropertyValues());
		}
		if (beanDefinition.hasConstructorArgumentValues()) {
			ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
			visitIndexedArgumentValues(cas.getIndexedArgumentValues());
			visitGenericArgumentValues(cas.getGenericArgumentValues());
		}
	}
	...
}
登录后复制

我们看到,在visitBeanDefinition()方法中,访问了其他数据,比如父类的名字、自己的类名、在IoC容器中的名称等各种信息。

以上是Java访问者模式的静态、动态和伪动态分派及实例分析的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

Java Spring 面试题 Java Spring 面试题 Aug 30, 2024 pm 04:29 PM

在本文中,我们保留了最常被问到的 Java Spring 面试问题及其详细答案。这样你就可以顺利通过面试。

突破或从Java 8流返回? 突破或从Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一种强大且表达力丰富的处理数据集合的方式。然而,使用Stream时,一个常见问题是:如何从forEach操作中中断或返回? 传统循环允许提前中断或返回,但Stream的forEach方法并不直接支持这种方式。本文将解释原因,并探讨在Stream处理系统中实现提前终止的替代方法。 延伸阅读: Java Stream API改进 理解Stream forEach forEach方法是一个终端操作,它对Stream中的每个元素执行一个操作。它的设计意图是处

PHP:网络开发的关键语言 PHP:网络开发的关键语言 Apr 13, 2025 am 12:08 AM

PHP是一种广泛应用于服务器端的脚本语言,特别适合web开发。1.PHP可以嵌入HTML,处理HTTP请求和响应,支持多种数据库。2.PHP用于生成动态网页内容,处理表单数据,访问数据库等,具有强大的社区支持和开源资源。3.PHP是解释型语言,执行过程包括词法分析、语法分析、编译和执行。4.PHP可以与MySQL结合用于用户注册系统等高级应用。5.调试PHP时,可使用error_reporting()和var_dump()等函数。6.优化PHP代码可通过缓存机制、优化数据库查询和使用内置函数。7

Java 中的时间戳至今 Java 中的时间戳至今 Aug 30, 2024 pm 04:28 PM

Java 中的时间戳到日期指南。这里我们还结合示例讨论了介绍以及如何在java中将时间戳转换为日期。

PHP与Python:了解差异 PHP与Python:了解差异 Apr 11, 2025 am 12:15 AM

PHP和Python各有优势,选择应基于项目需求。1.PHP适合web开发,语法简单,执行效率高。2.Python适用于数据科学和机器学习,语法简洁,库丰富。

Java程序查找胶囊的体积 Java程序查找胶囊的体积 Feb 07, 2025 am 11:37 AM

胶囊是一种三维几何图形,由一个圆柱体和两端各一个半球体组成。胶囊的体积可以通过将圆柱体的体积和两端半球体的体积相加来计算。本教程将讨论如何使用不同的方法在Java中计算给定胶囊的体积。 胶囊体积公式 胶囊体积的公式如下: 胶囊体积 = 圆柱体体积 两个半球体体积 其中, r: 半球体的半径。 h: 圆柱体的高度(不包括半球体)。 例子 1 输入 半径 = 5 单位 高度 = 10 单位 输出 体积 = 1570.8 立方单位 解释 使用公式计算体积: 体积 = π × r2 × h (4

PHP与Python:核心功能 PHP与Python:核心功能 Apr 13, 2025 am 12:16 AM

PHP和Python各有优势,适合不同场景。1.PHP适用于web开发,提供内置web服务器和丰富函数库。2.Python适合数据科学和机器学习,语法简洁且有强大标准库。选择时应根据项目需求决定。

PHP与其他语言:比较 PHP与其他语言:比较 Apr 13, 2025 am 12:19 AM

PHP适合web开发,特别是在快速开发和处理动态内容方面表现出色,但不擅长数据科学和企业级应用。与Python相比,PHP在web开发中更具优势,但在数据科学领域不如Python;与Java相比,PHP在企业级应用中表现较差,但在web开发中更灵活;与JavaScript相比,PHP在后端开发中更简洁,但在前端开发中不如JavaScript。

See all articles