大白话设计模式

发布时间 2023-04-12 11:06:33作者: gogoy

https://www.cnblogs.com/chanshuyi/p/quick-start-of-visitor-design-pattern.html

https://www.liaoxuefeng.com/wiki/1252599548343744/1281319659110433

 

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

01 什么是访问者模式?

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

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

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

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

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

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

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

接着声明一个 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 方法,将访问的流程固定死了,只有教你问什么的时候,我才会让你(记者)提问。

public class Scientist {

    private Visitor visitor;

    private String name;

    private Scientist(){}

    public Scientist(String name) {
        this.name = name;
    }

    public void accept(Visitor visitor) {
        this.visitor = visitor;
    }

    public void interview(){
        System.out.println("------------访问开始------------");
        System.out.println("---开始聊学校经历---");
        visitor.askSchoolExperience(name);
        System.out.println("---开始聊工作经历---");
        visitor.askWorkExperience(name);
        System.out.println("---开始聊科研成果---");
        visitor.askScienceAchievement(name);
    }
}

 

最后我们声明一个场景类 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() 方法,但这并不是绝对的。

需要注意的是,我们学习设计模式重点是理解类与类之间的关系,以及他们传递的信息。至于是通过什么方式传递的,是通过 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 }

 

可见,访问者模式的核心思想是为了访问比较复杂的数据结构不去改变数据结构,而是把对数据的操作抽象出来,在“访问”的过程中以回调形式在访问者中处理操作逻辑。如果要新增一组操作,那么只需要增加一个新的访问者。