大白话聊访问者模式:从入门到实践

开发 前端
说到访问,我们脑海中必定会想起新闻访谈,两个人面对面坐在一起。从字面上的意思理解:其实就相当于被访问者(某个公众人物)把访问者(记者)当成了外人,不想你随便动。你想要什么,我弄好之后给你(调用你的方法)。

[[382061]]

访问者模式,重点在于访问者二字。说到访问,我们脑海中必定会想起新闻访谈,两个人面对面坐在一起。从字面上的意思理解:其实就相当于被访问者(某个公众人物)把访问者(记者)当成了外人,不想你随便动。你想要什么,我弄好之后给你(调用你的方法)。

01 什么是访问者模式?

访问者模式的定义如下所示,说的是在不改变数据结构的提前下,定义新操作。

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。

但在实际的应用中,我发现有些例子并不是如此。有些例子中并没有稳定的数据结构,而是稳定的算法。在树义看来,访问者模式是:把不变的固定起来,变化的开放出去。

我们举生活中一个例子来聊聊:某科学家接受记者访谈。我们都知道科学家接受访问,肯定是有流程上的限制的,不可能让你随便问。我们假设这个过程是:先问科学家的学校经历,再聊你的工作经历,最后聊你的科研成果。那么在这个过程中,固定的是什么东西呢?固定的是接受采访的流程。变化的是什么呢?变化的是不同的记者,针对学校经历,可能会提不同的问题。

根据我们之前的理解,访问者模式其实就是要把不变的东西固定起来,变化的开放出去。那么对于科学家接受访谈这个事情,我们可以这么将其抽象化。

首先,我们需要有一个 Visitor 类,这里定义了一些外部(记者)可以做的事情(提学校经历、工作经历、科研成就的问题)。

  1. public interface Visitor { 
  2.     public void askSchoolExperience(String name); 
  3.     public void askWorkExperience(String name); 
  4.     public void askScienceAchievement(String name); 

接着声明一个 XinhuaVisitor 类去实现 Visitor 类,这表示是新华社的一个记者(访问者)想去访问科学家。

  1. public class XinhuaVisitor implements Visitor{ 
  2.     @Override 
  3.     public void askSchoolExperience(String name) { 
  4.         System.out.printf("请问%s:在学校取得的最大成就是什么?\n"name); 
  5.     } 
  6.  
  7.     @Override 
  8.     public void askWorkExperience(String name) { 
  9.         System.out.printf("请问%s:工作上最难忘的事情是什么?\n"name); 
  10.     } 
  11.  
  12.     @Override 
  13.     public void askScienceAchievement(String name) { 
  14.         System.out.printf("请问%s:最大的科研成果是什么?"name); 
  15.     } 

接着声明一个 Scientist 类,表明是一个科学家。科学家通过一个 accept () 方法接收记者(访问者)的访问申请,将其存储起来。科学家定义了一个 interview 方法,将访问的流程固定死了,只有教你问什么的时候,我才会让你(记者)提问。

  1. public class Scientist { 
  2.  
  3.     private Visitor visitor; 
  4.  
  5.     private String name
  6.  
  7.     private Scientist(){} 
  8.  
  9.     public Scientist(String name) { 
  10.         this.name = name
  11.     } 
  12.  
  13.     public void accept(Visitor visitor) { 
  14.         this.visitor = visitor; 
  15.     } 
  16.  
  17.     public void interview(){ 
  18.         System.out.println("------------访问开始------------"); 
  19.         System.out.println("---开始聊学校经历---"); 
  20.         visitor.askSchoolExperience(name); 
  21.         System.out.println("---开始聊工作经历---"); 
  22.         visitor.askWorkExperience(name); 
  23.         System.out.println("---开始聊科研成果---"); 
  24.         visitor.askScienceAchievement(name); 
  25.     } 

最后我们声明一个场景类 Client,来模拟访谈这一过程。

  1. public class Client { 
  2.     public static void main(String[] args) { 
  3.         Scientist yang = new Scientist("杨振宁"); 
  4.         yang.accept(new XinhuaVisitor()); 
  5.         yang.interview(); 
  6.     } 

运行的结果为:

  1. ------------访问开始------------ 
  2. ---开始聊学校经历--- 
  3. 请问杨振宁:在学校取得的最大成就是什么? 
  4. ---开始聊工作经历--- 
  5. 请问杨振宁:工作上最难忘的意见事情是什么? 
  6. ---开始聊科研成果--- 
  7. 请问杨振宁:最大的科研成果是什么? 

看到这里,大家对于访问者模式的本质有了更感性的认识(把不变的固定起来,变化的开放出去)。在这个例子中,不变的固定的就是访谈流程,变化的就是你可以提不同的问题。

一般来说,访问者模式的类结构如下图所示:

  • Visitor 访问者接口。访问者接口定义了访问者可以做的事情。这个需要你去分析哪些是可变的,将这些可变的内容抽象成访问者接口的方法,开放出去。而被访问者的信息,其实就是通过访问者的参数传递过去。
  • ConcreteVisitor 具体访问者。具体访问者定义了具体某一类访问者的实现。对于新华社记者来说,他们更关心杨振宁科学成果方面的事情,于是他们提问的时候更倾向于挖掘成果。但对于青年报记者来说,他们的读者是青少年,他们更关心杨振宁在学习、工作中的那种精神。
  • Element 具体元素。这里指的是具体被访问的类,在我们这个例子中指的是 Scientist 类。一般情况下,我们会提供一个 accept () 方法,接收访问者参数,将相当于接受其范文申请。但这个方法也不是必须的,只要你能够拿到 visitor 对象,你怎么定义这个参数传递都可以。

对于访问者模式来说,最重要的莫过于 Visitor、ConcreteVisitor、Element 这三个类了。Visitor、ConcreteVisitor 定义访问者具体能做的事情,被访问者的参数通过参数传递给访问者。Element 则通过各种方法拿到被访问者对象,常用的是通过 accept () 方法,但这并不是绝对的。

需要注意的是,我们学习设计模式重点是理解类与类之间的关系,以及他们传递的信息。至于是通过什么方式传递的,是通过 accept () 方法,还是通过构造函数,都不是重点。

02 访问者模式的实际应用

前面我们用一个生活的例子帮助大家理解访问者模式,相信大家对访问者模式应该有了个感性的理解了。为了回归编程实践本身,让大家对访问者模式能有更好的实践理解。下面我们将从软件编程上讲讲访问者模式在开源框架中的应用。

文件树遍历

JDK 中有文件操作,我们自然是清楚的。有文件操作,那自然就会有文件夹的遍历操作,即访问某个文件夹下面的所有文件或文件夹。试想一下,如果我们想要打印出某个文件夹下所有文件及文件夹的名字,我们需要怎么做?

很简单的做法,其实就是直接做一个树的遍历,然后将名字打印出来呀!

没错,这确实是正确答案!

那么如果我希望统计一下所有文件及文件夹的个数呢?

那就再遍历一次,然后用一个计数器去一直加一呗!

没错,这也是正确答案!

但你是否发现了这两个过程中,我们有一个相同的操作:遍历文件树。无论是打印文件名,还是计算文件树,我们都需要去遍历文件树。而无论哪一个过程,我们最终要的其实就是访问文件。

还记得我们说过设计模式的本质是什么吗?设计模式的本质是找出不变的东西,再找出变化的东西,然后找到合适的数据结构(设计模式)去承载这种变化。

在这个例子里,不变的东西是文件树的遍历,变化的是对于文件的不同访问操作。很显然,访问者模式是比较适合承载这种变化的。我们可以把这种不变的东西(文件树的遍历)固定起来,把变化的东西(文件的具体操作)开放出去。JDK 对于文件树的遍历,其实就是使用访问者模式实现的。

JDK 中声明了一个 FileVisitor 接口,定义了遍历者可以做的操作。

  1. public interface FileVisitor<T> { 
  2.     FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs); 
  3.     FileVisitResult visitFile(T file, BasicFileAttributes attrs) 
  4.         throws IOException; 
  5.     FileVisitResult visitFileFailed(T file, IOException exc) 
  6.         throws IOException; 
  7.     FileVisitResult postVisitDirectory(T dir, IOException exc) 
  8.         throws IOException; 

FileVisitor 中定义的 visitFile () 方法,其实就是对于文件的访问。被访问者(文件)的信息通过第一个参数 file 传递过来。这样遍历者就可以访问文件的内容了。

SimpleFileVisitor 则是对于 FileVisitor 接口的实现,该类中仅仅是做了简单的参数校验,并没有太过的逻辑。

  1. public class SimpleFileVisitor<T> implements FileVisitor<T> { 
  2.     @Override 
  3.     public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) 
  4.         throws IOException 
  5.     { 
  6.         Objects.requireNonNull(dir); 
  7.         Objects.requireNonNull(attrs); 
  8.         return FileVisitResult.CONTINUE
  9.     } 
  10.      
  11.     @Override 
  12.     public FileVisitResult visitFile(T file, BasicFileAttributes attrs) 
  13.         throws IOException 
  14.     { 
  15.         Objects.requireNonNull(file); 
  16.         Objects.requireNonNull(attrs); 
  17.         return FileVisitResult.CONTINUE
  18.     } 
  19.     //....其他省略 

FileVisitor 类和 SimpleFileVisitor 类对应的就是 UML 类图中的 Visitor 和 ConcreteVisitor 类。而 Element 元素,对应的其实是 JDK 中的 Files 类。

Files 文件中遍历文件树是通过 walkFileTree () 方法实现的。在 walkFileTree () 方法中实现了树的遍历,在遍历到文件的时候会通过 visitor 类的 visitFile 方法调用遍历者的方法,将遍历到的文件传递给遍历者,从而达到分离变化的目的。

ASM 修改字节码

ASM 是 Java 的字节码增强技术,这里面就用到了访问者模式,主要是用来进行字节码的修改。在 ASM 中与此相关的三个类分别是:ClassReader、ClassVisitor、ClassWriter。

ClassReader 类相当于访问者模式中的 Element 元素。它将字节数组或 class 文件读入内存中,并以树的数据结构表示。该类定义了一个 accept 方法用来和 visitor 交互。

ClassVisitor 相当于抽象访问者接口。ClassReader 对象创建之后,需要调用 accept () 方法,传入一个 ClassVisitor 对象。在 ClassReader 的不同时期会调用 ClassVisitor 对象中不同的 visit () 方法,从而实现对字节码的修改。

ClassWriter 是 ClassVisitor 的是实现类,它负责将修改后的字节码输出为字节数组。

对于 ASM 这种场景而言,字节码规范是非常严格且稳定的,如果随便更改可能出问题。但我们又需要对字节码进行动态修改,从而达到某些目的。在这种情况下,ASM 的设计者采用了访问者模式将变化的部分隔离开来,将不变的部分固定下来,从而达到了灵活扩展的目的。

03 我们该如何使用?

从上面几个例子,我们大致可以明白访问者模式的使用场景:某些较为稳定的东西(数据结构或算法),不想直接被改变但又想扩展功能,这时候适合用访问者模式。

说到对于访问者模式使用场景的定义,我们会觉得模板方法模式与这个使用场景的定义很像。但它们还是有些许差别的。访问者模式的变化与非变化(即访问者与被访问者)之间,它们只是简单的包含关系,而模板方法模式的变化与非变化则是继承关系。 但它们也确实有类似的地方,即都是封装了固定不变的东西,开放了变动的东西。

访问者模式的优点很明显,即隔离了变化的东西,固定了不变的东西,使得整体的可维护性更强、具有更强的扩展性。但它也带来了设计模式通用的一些缺点,例如:

  • 类结构变得复杂。之前我们可是简单的调用关系,现在则是多个类之间的继承和组合关系。从一定程度上,提高了对开发人员的要求,提高了研发成本。
  • 被访问者的变更变得更加困难。例如我们上面科学家访谈的例子,如果科学家访谈希望新增一个环节,那么 Scientist 类需要修改,Visitor 类、XinhuaVisitor 类都需要修改。

有这些多优点,但也有这么多缺点,那实际工作中我们应该怎么判断是否用访问者模式呢?总的原则就是扬长避短,即当场景完全利用了访问者模式的优点,规避了访问者模式的缺点的时候,就是使用访问者模式的最佳时机。

虽然使用访问者模式会让被访问者的变更变得更加困难,但如果被访问者很稳定,基本不会变更,那这个缺点不就去除了么。例如在 ASM 的例子中,元素是 ClassReader,其存储了字节码的结构。而字节码结构完全不会轻易改变,所以在这个「被访问者的变更变得更加困难」的缺点也就不存在了。

而「类结构变得复杂」这个缺点,则是需要根据当时业务的复杂程度来看的。如果当时业务很简单,而且变化也不大,那么使用设计模式完全是多余的。但是如果当时业务很复杂了,我们还是在一个类里做修改,那么很大可能性会出大问题。这时候就需要用设计模式来承载复杂的业务结构了。

本文转载自微信公众号「陈树义」,可以通过以下二维码关注。转载本文请联系陈树义公众号。

 

责任编辑:武晓燕 来源: 陈树义
相关推荐

2020-02-20 11:32:09

Kafka概念问题

2022-03-04 09:28:29

代码访问者模式软件开发

2020-02-04 15:00:25

大白话认识JVM

2023-12-26 18:22:05

RocketMQ延迟消息

2021-10-08 08:58:35

物联网通信发布者

2020-12-04 06:40:46

Zookeeper选举机制

2019-05-17 08:27:23

SQL注入漏洞攻击

2023-09-13 09:02:22

PVPVC存储

2021-03-01 18:38:32

Mock测试软件

2023-10-27 10:17:46

设计模式访问者元素

2019-08-14 09:13:38

中台互联网业务

2020-11-10 16:00:55

机器学习人工智能AI

2024-04-24 12:41:10

Rust安全性内存

2020-12-11 14:02:58

机器学习分类算法回归算法

2023-09-18 14:34:07

Kubernetes云原生

2022-11-26 00:00:06

装饰者模式Component

2023-02-28 09:10:28

设计模式解耦数据

2020-11-10 09:20:40

开发模式代码

2023-09-18 15:49:40

Ingress云原生Kubernetes

2021-01-27 13:50:17

AI 数据机器学习
点赞
收藏

51CTO技术栈公众号