《设计模式就该这样学》之彻底搞懂访问者模式的静态、动态和伪动态分派(1)

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

}

@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 employees = new LinkedList();

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());

}

运行结果如下图所示。

图片

在上述案例中,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 {

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:这个访问结果表示当前的遍历过程将会继续。

线程、数据库、算法、JVM、分布式、微服务、框架、Spring相关知识

一线互联网P7面试集锦+各种大厂面试集锦

学习笔记以及面试真题解析

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

}

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

线程、数据库、算法、JVM、分布式、微服务、框架、Spring相关知识

[外链图片转存中…(img-nZ0ij2ma-1713430038321)]

一线互联网P7面试集锦+各种大厂面试集锦

[外链图片转存中…(img-X4T3plfv-1713430038322)]

学习笔记以及面试真题解析

[外链图片转存中…(img-ArnOhz7z-1713430038324)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-1TdIT1PB-1713430038325)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值