郑州模板建站,零基础学软件开发需要多久,网站通知发送邮件,织梦做的网站打不开网页访问者模式
访问者模式被称为是最复杂的设计模式#xff0c;比较难理解并且使用频率不高。
在 GoF 的《设计模式》⼀书中#xff0c;访问者者模式(Visitor Design Pattern#xff09;是这么定义的#xff1a; Allows for one or more operation to be applied to a set o…访问者模式
访问者模式被称为是最复杂的设计模式比较难理解并且使用频率不高。
在 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 ListResourceFile 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 MapString, Executor 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? super Path 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 FileVisitorT {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 SimpleFileVisitorT implements FileVisitorT {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里就重写了preVisitDirectory和visitFile方法 Files.walkFileTree(packageDir, opts, maxDepth,new SimpleFileVisitorPath() {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;}});总结
简单来说访问者模式就是封装一些作用于某种数据结构中的各元素的操作它可以在不改变这个数据结构的前提下定义作用于这些元素的新操作。
访问者模式适用场景 :
数据结构稳定但是作用于数据结构的操作经常变化需要数据结构与数据操作分离需要对不同数据类型(元素)进行操作但是有不使用if.. else ..判断具体类型
优点
使得数据结构和作用于结构上的操作解耦使得操作集合可以独立变化。扩展性好添加新的操作或者说访问者会非常容易。
缺点
增加新的元素类型会非常困难每次新增元素类型则访问者类必须增加对应元素类型的操作违反了依赖倒置原则访问者依赖的是具体元素类型而不是抽象变更元素的属性可能会导致对应的访问者类也需要修改