大白话设计模式
https://www.cnblogs.com/chanshuyi/p/quick-start-of-visitor-design-pattern.html
https://www.liaoxuefeng.com/wiki/1252599548343744/1281319659110433
访问者模式,重点在于访问者二字。说到访问,我们脑海中必定会想起新闻访谈,两个人面对面坐在一起。从字面上的意思理解:其实就相当于被访问者(某个公众人物)把访问者(记者)当成了外人,不想你随便动。你想要什么,我弄好之后给你(调用你的方法)。
01 什么是访问者模式?
访问者模式的定义如下所示,说的是在不改变数据结构的提前下,定义新操作。
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
但在实际的应用中,我发现有些例子并不是如此。有些例子中并没有稳定的数据结构,而是稳定的算法。在树义看来,访问者模式是:把不变的固定起来,变化的开放出去。
我们举生活中一个例子来聊聊:某科学家接受记着访谈。我们都知道科学家接受访问,肯定是有流程上的限制的,不可能让你随便问。我们假设这个过程是:先问科学家的学校经历,再聊你的工作经历,最后聊你的科研成果。那么在这个过程中,固定的是什么东西呢?固定的是接受采访的流程。变化的是什么呢?变化的是不同的记者,针对学校经历,可能会提不同的问题。
根据我们之前的理解,访问者模式其实就是要把不变的东西固定起来,变化的开放出去。那么对于科学家接受访谈这个事情,我们可以这么将其抽象化。
首先,我们需要有一个 Visitor 类,这里定义了一些外部(记者)可以做的事情(提学校经历、工作经历、科研成就的问题)。
public interface Visitor { public void askSchoolExperience(String name); // 被访问者的参数通过参数传递给访问者,此处name是被访问者的姓名 public void askWorkExperience(String name); 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 } 16 }
接着声明一个 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) { // 被访问者通过 accept(Vistor t) 方法 拿到访问者对象 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 } 26 }
最后我们声明一个场景类 Client,来模拟访谈这一过程。
public class Client { public static void main(String[] args) { Scientist yang = new Scientist("杨振宁"); yang.accept(new XinhuaVisitor()); yang.interview(); } }
运行的结果为:
------------访问开始------------
---开始聊学校经历---
请问杨振宁:在学校取得的最大成就是什么?
---开始聊工作经历---
请问杨振宁:工作上最难忘的意见事情是什么?
---开始聊科研成果---
请问杨振宁:最大的科研成果是什么?
看到这里,大家对于访问者模式的本质有了更感性的认识(把不变的固定起来,变化的开放出去)。在这个例子中,不变的固定的就是访谈流程,变化的就是你可以提不同的问题。
一般来说,访问者模式的类结构如下图所示:
- Visitor 访问者接口。访问者接口定义了访问者可以做的事情。这个需要你去分析哪些是可变的,将这些可变的内容抽象成访问者接口的方法,开放出去。而被访问者的信息,其实就是通过访问者的参数传递过去。
- ConcreteVisitor 具体访问者。具体访问者定义了具体某一类访问者的实现。对于新华社记者来说,他们更关心杨振宁科学成果方面的事情,于是他们提问的时候更倾向于挖掘成果。但对于青年报记者来说,他们的读者是青少年,他们更关心杨振宁在学习、工作中的那种精神。
- Element 具体元素。这里指的是具体被访问的类,在我们这个例子中指的是 Scientist 类。一般情况下,我们会提供一个 accept() 方法,接收访问者参数,将相当于接受其范文申请。但这个方法也不是必须的,只要你能够拿到 visitor 对象,你怎么定义这个参数传递都可以。
对于访问者模式来说,最重要的莫过于 Visitor、ConcreteVisitor、Element 这三个类了。Visitor、ConcreteVisitor 定义访问者具体能做的事情,被访问者的参数通过参数传递给访问者。Element(具体被访问的对象) 则通过各种方法拿到访问者对象,常用的是通过 accept(Vistor t) 方法 拿到访问者对象,但这并不是绝对的。
需要注意的是,我们学习设计模式重点是理解类与类之间的关系,以及他们传递的信息。至于是通过什么方式传递的,是通过 accept() 方法,还是通过构造函数,都不是重点。
简化的访问者模式到JDK里面的实现
这里我们只介绍简化的访问者模式。假设我们要递归遍历某个文件夹的所有子文件夹和文件,然后找出.java
文件,正常的做法是写个递归:
void scan(File dir, List<File> collector) {
for (File file : dir.listFiles()) {
if (file.isFile() && file.getName().endsWith(".java")) {
collector.add(file);
} else if (file.isDir()) {
// 递归调用:
scan(file, collector);
}
}
}
上述代码的问题在于,扫描目录的逻辑和处理.java文件的逻辑混在了一起。如果下次需要增加一个清理.class
文件的功能,就必须再重复写扫描逻辑。
因此,访问者模式先把数据结构(这里是文件夹和文件构成的树型结构)和对其的操作(查找文件)分离开,以后如果要新增操作(例如清理.class
文件),只需要新增访问者,不需要改变现有逻辑。
用访问者模式改写上述代码步骤如下:
首先,我们需要定义访问者接口,即该访问者能够干的事情:
1 public interface Visitor { 2 // 访问文件夹: 3 void visitDir(File dir); 4 // 访问文件: 5 void visitFile(File file); 6 }
紧接着,我们要定义能持有文件夹和文件的数据结构FileStructure
:
1 public class FileStructure { 2 // 根目录: 3 private File path; 4 public FileStructure(File path) { 5 this.path = path; 6 } 7 }
然后,我们给FileStructure
增加一个handle()
方法,传入一个访问者:
1 public class FileStructure { 2 ... 3 4 public void handle(Visitor visitor) { 5 scan(this.path, visitor); 6 } 7 8 private void scan(File file, Visitor visitor) { 9 if (file.isDirectory()) { 10 // 让访问者处理文件夹: 11 visitor.visitDir(file); 12 for (File sub : file.listFiles()) { 13 // 递归处理子文件夹: 14 scan(sub, visitor); 15 } 16 } else if (file.isFile()) { 17 // 让访问者处理文件: 18 visitor.visitFile(file); 19 } 20 } 21 }
这样,我们就把访问者的行为抽象出来了。如果我们要实现一种操作,例如,查找.java
文件,就传入JavaFileVisitor
:
1 FileStructure fs = new FileStructure(new File(".")); 2 fs.handle(new JavaFileVisitor());
这个JavaFileVisitor
实现如下:
1 public class JavaFileVisitor implements Visitor { 2 public void visitDir(File dir) { 3 System.out.println("Visit dir: " + dir); 4 } 5 6 public void visitFile(File file) { 7 if (file.getName().endsWith(".java")) { 8 System.out.println("Found java file: " + file); 9 } 10 } 11 }
类似的,如果要清理.class
文件,可以再写一个ClassFileClearnerVisitor
:
1 public class ClassFileCleanerVisitor implements Visitor { 2 public void visitDir(File dir) { 3 } 4 5 public void visitFile(File file) { 6 if (file.getName().endsWith(".class")) { 7 System.out.println("Will clean class file: " + file); 8 } 9 } 10 }
可见,访问者模式的核心思想是为了访问比较复杂的数据结构,不去改变数据结构,而是把对数据的操作抽象出来,在“访问”的过程中以回调形式在访问者中处理操作逻辑。如果要新增一组操作,那么只需要增加一个新的访问者。
JDK类库
FileVisitor 类和 SimpleFileVisitor 类对应的就是 UML 类图中的 Visitor 和 ConcreteVisitor 类。而 Element 元素,对应的其实是 JDK 中的 Files 类。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏