【设计模式】访问者模式
创始人
2024-06-02 05:32:06
0

访问者模式

访问者模式被称为是最复杂的设计模式,比较难理解并且使用频率不高。

在 GoF 的《设计模式》⼀书中,访问者者模式(Visitor Design Pattern)是这么定义的:

Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure.

允许⼀个或者多个操作应⽤到⼀组对象上,解耦操作和对象本⾝。
访问者模式是一种将数据结构和数据操作分离的设计模式,属于行为型模式。

访问者模式的基本思想是: 假设系统中有一个由许多对象构成的对象结构(元素),这些对象的类都提供一个accept()方法用来接受访问者对象的访问,不同的访问者访问同一对象可以产生不同的数据结果(访问者其实就是一个拥有visit()方法的接口) 。accept()方法可以接收不同的访问者对象,然后在其内部将自己(元素)转发到访问者对象的visit()方法内。

访问者模式的核心是解耦数据结构和数据操作,使得对元素的操作具备良好的扩展性。可以通过扩展不同的访问者来实现对同一元素集的不同操作。

如果你的系统中只是对单个对象(类)进行操作或者对多个类进行一种操作,那么就没必要使用访问者模式了。运用访问者模式是为了方便后续扩展操作类型,在对对象集(多个类对象)扩展操作的时候可以不需要修改所有类的代码。

当系统中存在类型数目稳定(固定)的一类数据结构时,可以通过访问者模式方便地实现对该类型所有数据结构的不同操作。

访问者模式类图

在这里插入图片描述

主要包含四个角色 :
Visitor 抽象访问者:接口或者抽象类,它定义了对每一个可访问元素(Visitable Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变(不能改变的意思是说,如果元素类的个数经常改变,则说明不适合使用访问者模式)。
ConcreteVisitor:具体的访问者,实现对每一个元素的具体操作。
Element 抽象元素:元素接口,它定义了一个接受访问者(accept)的方法,其意义是指,每一个元素都要可以被访问者访问。
ConcreteElement:具体的元素类,它提供接受访问者的具体实现,而这个具体的实现,通常情况下是调用访问者提供的访问该元素类的方法。

代码示例

我们使用访问者模式模拟一个处理不同类型文件的场景
假设我们有三类文件,PDF,EXCEL,WORD,我们需要从三种文件中提取信息存到到自己的系统里(假设需要导入到自己的一个txt文件),然后还需要对三类文件都进行压缩等一系列功能

将不同文件类型定义为抽象元素(这是一个稳定的数据结构), 对文件的操作定义为访问者,代码如下 :

public abstract class ResourceFile {private String name;protected ResourceFile(String name) {this.name = name;}abstract void accept(Vistor vistor);}
public class PDFFile extends ResourceFile {protected PDFFile(String name) {super(name);}@Overridevoid accept(Vistor vistor) {vistor.visit(this);}
}public class ExcelFile extends ResourceFile{protected ExcelFile(String name) {super(name);}@Overridevoid accept(Vistor vistor) {vistor.visit(this);}
}public class WordFile extends ResourceFile{public WordFile(String name) {super(name);}@Overridevoid accept(Vistor vistor) {vistor.visit(this);}
}
public interface Vistor {void visit(PDFFile file);void visit(ExcelFile file);void visit(WordFile file);}
public class CompressionVistor implements Vistor{@Overridepublic void visit(PDFFile file) {System.out.println("压缩pdf文件");}@Overridepublic void visit(ExcelFile file) {System.out.println("压缩excel文件");}@Overridepublic void visit(WordFile file) {System.out.println("压缩word文件");}
}
public class ExtractVistor implements Vistor{@Overridepublic void visit(PDFFile file) {System.out.println("提取pdf文字内容");}@Overridepublic void visit(ExcelFile file) {System.out.println("提取excel文字内容");}@Overridepublic void visit(WordFile file) {System.out.println("提取word文字内容");}
}
public class Test {private static final List resourceFileList = new ArrayList<>();static {resourceFileList.add(new PDFFile("设计模式.pdf"));resourceFileList.add(new ExcelFile("Data.excel"));resourceFileList.add(new WordFile("笔记.doc"));}public static void main(String[] args) {for (ResourceFile resourceFile: resourceFileList) {resourceFile.accept(new CompressionVistor());}for (ResourceFile resourceFile: resourceFileList) {resourceFile.accept(new ExtractVistor());}}}

Double Dispatch

静态分派

静态分派(Static Dispatch)就是按照变量的静态类型(变量被声明时的类型)进行分派,从而确定方法的执行版本,静态分派在编译时就可以确定方法的版本,典型例子就是java的方法重载
java在静态分派的时候,我们可以根据多个判断依据(即参数个数和参数类型)判断使用哪个方法,所以java是静态多分派的语言

动态分派

动态分派,不是在编译期确定方法版本,而是在运行时才能确定

Single Dispatch,指的是我们仅仅需要根据对象运行时的类型来决定执行哪个对象的方法

Double Dispatch,指的是我们需要根据对象的运行时类型和参数的运行时类型来决定执行哪个对象的哪个方法 (二者区别主要在于是否可以根据方法参数运行时的类型来判断执行对象的哪个方法)

当前主流的⾯向对象编程语⾔(⽐如,Java、C++、C#)都只⽀持 Single Dispatch,不⽀持 Double Dispatch。

以Java为例,Java⽀持多态,代码可以在运⾏时获得对象的实际类型,然后根据实际类型决定调⽤哪个对象的方法。 Java 也⽀持方法重载,但 Java 设计的方法重载的语法规则是在编译时,根据传递进函数的参数的声明类型,来决定调⽤哪个重载方法。也就是说,具体执⾏哪个对象的哪个⽅法,只跟对象的运⾏时类型有关,跟参数的运⾏时类型⽆关。所以,Java 语⾔是 动态单分派的语言。

我们可以看下具体的例子 :

public class ParentClass {public void method() {System.out.println("ParentClass 执行method方法");}}public class SonClass extends ParentClass{@Overridepublic void method() {System.out.println("SonClass 执行method方法");}
}
public class SingleDispatch {public void method(ParentClass parentClass) {parentClass.method();}public void print(ParentClass parentClass) {System.out.println("打印parentClass");}public void print(SonClass sonClass) {System.out.println("打印sonClass");}
}
public class Test {public static void main(String[] args) {ParentClass s = new SonClass();SingleDispatch singleDispatch = new SingleDispatch();singleDispatch.method(s);//执⾏哪个对象的⽅法,由对象的实际类型决定(多态)singleDispatch.print(s);//执⾏对象的哪个⽅法,由参数对象的声明类型决定,这里声明的时ParentClass类型}}

在这里插入图片描述

动态双分派的语言不需要访问者模式

假设 Java 语⾔⽀持 动态双分派,那么下面的代码就可以编译通过,正常执行了。

public class ExtractExecutor {public void extract(PDFFile file) {System.out.println("提取pdf文字内容");}public void extract(WordFile file) {System.out.println("提取word文字内容");}public void extract(ExcelFile file) {System.out.println("提取excel文字内容");}}
    public static void main(String[] args) {ExtractExecutor extractExecutor = new ExtractExecutor();for (ResourceFile resourceFile: resourceFileList) {//这里会编译报错: Cannot resolve method 'extract(ResourceFile)'extractExecutor.extract(resourceFile);}}

代码会在运⾏时,根据参数(resourceFile)的实际类型(PDFFile、ExcelFile、WordFile),来决定调用extract()的三个重载方法中的哪⼀个,也就不需要访问者模式了。

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

所谓的动态双分派就是在运行时根据对象和参数的运行时类型去判断调用哪个一个对象的哪个方法。访问者模式通过进行两次动态单分派来达到这个效果。

for (ResourceFile resourceFile: resourceFileList) {resourceFile.accept(new ExtractVistor());}
  @Overridevoid accept(Vistor vistor) {vistor.visit(this);}

当调用accept()方法的时候, 根据resourceFile的实际类型决定调用哪个文件的accept()方法;
在执行accept()方法的时候,根据vistor的示例类型来决定调用哪个Vistor的visist方法,此时的this的类型就是这个类的静态类型,这是在编译期就确定的,所以也可以确定是调用的哪个重载方法

通过工厂模式实现上述功能

上述的例子,如果对文件的操作也比较固定,也可以使用工厂模式来实现,定义⼀个包含 extract() 接⼝的Executor接⼝。PdfExtractExecutor、ExcelExtractExecutor、WordExtractExecutor 类实现 Executor接⼝,完成对各自⽂件的⽂本内容抽取。然后再提供一个ExtractExecutorFactory ⼯⼚类根据不同的⽂件类型,返回不同的 Executor。

public abstract class ResourceFile {private String name;protected ResourceFile(String name) {this.name = name;}abstract String getType();
}
public class PDFFile extends ResourceFile {protected PDFFile(String name) {super(name);}@OverrideString getType() {return "PDF";}}
public interface Executor {void extract(ResourceFile file);
}
//省略了WordExtractExecutor,ExcelExtractExecutor的代码
public class PDFExtractExecutor implements Executor{@Overridepublic void extract(ResourceFile file) {System.out.println("提取pdf的内容");}
}
public class ExtractExecutorFactory {private static Map map = new HashMap<>();static {map.put("PDF", new PDFExtractExecutor());
//        map.put("EXCEL", new ExeclExtractExecutor());
//        map.put("WORD", new WordExtractExecutor());}public static Executor getExecutor(ResourceFile file) {return map.get(file.getType());}}

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

Java 7 版本后,Files 类提供了 walkFileTree() 方法,该方法可以很容易的对目录下的所有文件进行遍历,需要 Path、FileVisitor 两个参数。其中,Path 是要遍历文件的路径,FileVisitor 则可以看成一个文件访问器。源码如下。

package java.nio.file;
public final class Files {...public static Path walkFileTree(Path start, FileVisitor visitor)throws IOException{return walkFileTree(start,EnumSet.noneOf(FileVisitOption.class),Integer.MAX_VALUE,visitor);}...
}

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

FileVisitor 主要提供了 4 个方法,且返回结果的都是 FileVisitResult 对象值,用于决定当前操作完成后接下来该如何处理。FileVisitResult 是一个枚举类,代表返回之后的一些后续操作。

package java.nio.file;import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
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;
}package java.nio.file;public enum FileVisitResult {CONTINUE,TERMINATE,SKIP_SUBTREE,SKIP_SIBLINGS;
}

FileVisitResult 主要包含 4 个常见的操作。

  • FileVisitResult.CONTINUE:这个访问结果表示当前的遍历过程将会继续。
  • FileVisitResult.SKIP_SIBLINGS:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前文件/目录的兄弟节点。
  • FileVisitResult.SKIP_SUBTREE:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前目录下的所有节点。
  • FileVisitResult.TERMINATE:这个访问结果表示当前的遍历过程将会停止。

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

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

一开始觉得这里的设计比较多余,后来仔细想了下,在不同场景下我们对文件树的遍历要求是不一样的,通过访问者模式,用户可以方便的定义自己的遍历操作

比方说在JavacPathFileManager里就重写了preVisitDirectoryvisitFile方法

    Files.walkFileTree(packageDir, opts, maxDepth,new SimpleFileVisitor() {@Overridepublic FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {Path name = dir.getFileName();if (name == null || SourceVersion.isIdentifier(name.toString())) // JSR 292?return FileVisitResult.CONTINUE;elsereturn FileVisitResult.SKIP_SUBTREE;}@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {if (attrs.isRegularFile() && kinds.contains(getKind(file.getFileName().toString()))) {JavaFileObject fe =PathFileObject.createDirectoryPathFileObject(JavacPathFileManager.this, file, pathDir);results.append(fe);}return FileVisitResult.CONTINUE;}});

总结

简单来说,访问者模式就是封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新操作。

访问者模式适用场景 :

  1. 数据结构稳定,但是作用于数据结构的操作经常变化
  2. 需要数据结构与数据操作分离
  3. 需要对不同数据类型(元素)进行操作,但是有不使用if.. else ..判断具体类型

优点:

  1. 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化。
  2. 扩展性好,添加新的操作或者说访问者会非常容易。

缺点:

  1. 增加新的元素类型会非常困难,每次新增元素类型,则访问者类必须增加对应元素类型的操作
  2. 违反了依赖倒置原则,访问者依赖的是具体元素类型,而不是抽象
  3. 变更元素的属性可能会导致对应的访问者类也需要修改

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...