访问者模式
定义
其官方定义为:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。
小编的理解为它能将对对象的算法(操作)与对象完全隔离开来,即:使用访问者访问对象。
需求
1.我们要遍历一个目录的所有文件并输出文件名称,我们使用递归操作。
private void readFile(File file) {
System.out.println(file.getName());
if(file.isDirectory()){
for (File file1 : Objects.requireNonNull(file.listFiles())) {
readFile(file1);
}
}
}
2.在1需求的情况下统计java类的数量则我们使用以下的方法
int count = 0;
private void readFile(File file) {
System.out.println(file.getName());
if (file.getName().endsWith(".java")) {
count++;
}
if (file.isDirectory()) {
for (File file1 : Objects.requireNonNull(file.listFiles())) {
readFile(file1);
}
}
}
以上虽然完成了我们的需求但是从设计角度产生几个问题
a、 违反了职责单一原则。
b、为了解决职责单一则需要将2的需求重新写一个方法,这样就会多次遍历。
c、随着需求的增多则编码复杂度提高。
** 现在我们引入访问者模式 **
FileWrapper:是一个文件包装类
FileVisitor:为文件访问器
我们通过文件访问器去访问文件。是如下代码
public class FileWrapper {
private File file;
public FileWrapper(File file) {
this.file = file;
}
public void accept(FileVisitor visitor) {
doAccept(file, visitor);
}
private void doAccept(File file, FileVisitor visitor) {
visitor.visit(file);
if (file.isDirectory()) {
for (File file1 : Objects.requireNonNull(file.listFiles())) {
doAccept(file1, visitor);
}
}
}
}
public interface FileVisitor {
/**
* 文件操作
* @param file 文件
*/
void visit(File file);
}
public class VisitorTest {
private static final String FILE_SUFFIX = ".java";
int count = 0;
@Test
public void fileTest() {
File file = new File("D:\\workspace\\designlecture");
FileWrapper fileWrapper = new FileWrapper(file);
//遍历并打印文件名称
fileWrapper.accept(acceptFile -> System.out.println(acceptFile.getName()));
//统计java文件数量
fileWrapper.accept(acceptFile -> {
if (acceptFile.getName().endsWith(FILE_SUFFIX)) {
count++;
}
});
}
}
上面解决了单一原则,以及不同需求其操作的隔离,不过仍然遍历了两次,那么怎么解决遍历两次的问题
目前小编想到两种方式:
一种先实现FileVisitor接口,废话不多说看代码
public class FileVisitorGroup implements FileVisitor {
private List<FileVisitor> fileVisitorList;
public FileVisitorGroup(List<FileVisitor> fileVisitorList) {
this.fileVisitorList = fileVisitorList;
}
@Override
public void visit(File file) {
for (FileVisitor fileVisitor : fileVisitorList) {
fileVisitor.visit(file);
}
}
}
修改FileWrapper
public class FileWrapper {
private File file;
public FileWrapper(File file) {
this.file = file;
}
public void accept(FileVisitor visitor) {
doAccept(file, visitor);
}
public void accept(FileVisitorGroup visitor) {
doAccept(file, visitor);
}
private void doAccept(File file, FileVisitor visitor) {
visitor.visit(file);
if (file.isDirectory()) {
for (File file1 : Objects.requireNonNull(file.listFiles())) {
doAccept(file1, visitor);
}
}
}
}
修改test方法
public class VisitorTest {
private static final String FILE_SUFFIX = ".java";
int count = 0;
@Test
public void fileTest() {
File file = new File("D:\\workspace\\designlecture");
//遍历并打印文件名称
FileWrapper fileWrapper = new FileWrapper(file);
FileVisitor nameFileVisitor = acceptFile -> System.out.println(acceptFile.getName());
FileVisitor javaFileVisitor = acceptFile -> {
if (acceptFile.getName().endsWith(FILE_SUFFIX)) {
count++;
}
};
List<FileVisitor> fileVisitors = Arrays.asList(nameFileVisitor,javaFileVisitor);
fileWrapper.accept(new FileVisitorGroup(fileVisitors));
}
}
第二种实现方式:使用链表,将FileVisitor接口改成抽象类
public abstract class AbstractFileVisitor {
private AbstractFileVisitor fileVisitor;
/**
* 访问文件处理方法
*
* @param file 文件
*/
public abstract void visit(File file);
public void chainVisit(File file){
visit(file);
if(Objects.nonNull(fileVisitor)){
fileVisitor.visit(file);
}
}
}
修改文件包装类FileWrapper
public class FileWrapper {
private File file;
public FileWrapper(File file) {
this.file = file;
}
public void accept(AbstractFileVisitor visitor) {
doAccept(file, visitor);
}
private void doAccept(File file, AbstractFileVisitor visitor) {
visitor.chainVisit(file);
if (file.isDirectory()) {
for (File file1 : Objects.requireNonNull(file.listFiles())) {
doAccept(file1, visitor);
}
}
}
}
测试类修改
public class VisitorTest {
private static final String FILE_SUFFIX = ".java";
private int count = 0;
@Test
public void chainTest(){
File file = new File("D:\\workspace\\designlecture");
//遍历并打印文件名称
FileWrapper fileWrapper = new FileWrapper(file);
AbstractFileVisitor nameFileVisitor = new AbstractFileVisitor() {
@Override
public void visit(File file) {
System.out.println(file.getName());
}
};
AbstractFileVisitor javaFileVisitor = new AbstractFileVisitor() {
@Override
public void visit(File file) {
if(file.getName().endsWith(FILE_SUFFIX)){
count++;
}
}
};
nameFileVisitor.setFileVisitor(javaFileVisitor);
fileWrapper.accept(nameFileVisitor);
System.out.println(count);
}
}
到这儿访问者模式案例基本差不多了,不知道小编说明白了吗?
其实jdk自带了Files工具包,更加全面,大家可以看看他的访问者模式是怎样设计,其功能很强大。先画个类图给大家介绍一下吧
上面是jdk访问文件的主要功能,大家有兴趣的话可以看看源码,小编在这儿写一个测试类,大家有兴趣可以根据测试类去打断点调试。
@Test
public void jdkFileVisitor() throws IOException {
File file = new File("D:\\workspace\\designlecture");
Files.walkFileTree(file.toPath(), new java.nio.file.FileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("开始访问"+dir);
if(dir.toFile().getName().equals("target")){
return FileVisitResult.SKIP_SIBLINGS;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println("结束访问"+dir);
return FileVisitResult.CONTINUE;
}
});
}
打印结果,这边打印是一个文件一个文件的扫描,然后数据结构是栈,先进后出。
复杂应用场景 AMS
** 注意 :访问者模式基本讲完了,下面是对ams的一些简单应用,如果不感兴趣的小伙伴可以直接跳到总结。**
ASM a very small and fast Java bytecode manipulation framework。
ASM是一个JAVA字节码分析、创建和修改的开源应用框架。
ASM里面所用的一种设计模式也是访问者模式
AMS使用场景非常对,比方说springMVC对传入参数名的映射,jacoco代码分析,spring aop原理中cglib也是基于它实现的,包括javasist,mybatis,dubbo等等。
下面是简单用例以及相关的讲解
上图:
第一张图为calss文件的基本构成,然后读取到AMS,在通过AMS里面的各种访问器进行读取,上代码
public class AmsTest {
@Test
public void ExampleTest() throws IOException {
byte[] bytes = Files.readAllBytes(new File("D:\\workspace\\designlecture\\target" +
"\\classes\\com\\learn\\code\\pattern\\visitor\\Example.class").toPath());
ClassReader classReader = new ClassReader(bytes);
classReader.accept(new ClassVisitor(Opcodes.ASM5) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
}
//访问方法
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println(name);//方法名称
return new MethodVisitor(Opcodes.ASM5) {
//获取方法的所有局部标量表
@Override
public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
System.out.println(name);
}
};
}
},ClassReader.EXPAND_FRAMES);
}
}
用法简单粗暴,里面有很多方法,包括读取代码行数等等。挺厉害的。
第二张图主要为拿到文件读取之后对文件里面的内容进行修改完毕然后输出新的文件,上代码
@Test
public void writerTest() throws IOException {
byte[] bytes = Files.readAllBytes(new File("D:\\workspace\\designlecture\\target" +
"\\classes\\com\\learn\\code\\pattern\\visitor\\Example.class").toPath());
ClassReader classReader = new ClassReader(bytes);
ClassWriter classWriter = new ClassWriter(Opcodes.ASM5);
classReader.accept(classWriter,ClassReader.EXPAND_FRAMES);
byte[] bytes1 = classWriter.toByteArray();
Files.write(new File("D:\\workspace\\designlecture\\target" +
"\\classes\\com\\learn\\code\\pattern\\visitor\\Example2.class").toPath(),bytes1);
}
这里只是完整的读取然后直接写出去了,并没有对里面属性或方法的修改。
@Test
public void modifyWriterTest() throws IOException {
byte[] bytes = Files.readAllBytes(new File("D:\\workspace\\designlecture\\target" +
"\\classes\\com\\learn\\code\\pattern\\visitor\\Example.class").toPath());
ClassReader classReader = new ClassReader(bytes);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classReader.accept(classWriter, ClassReader.EXPAND_FRAMES);
classReader.accept(new ClassVisitor(Opcodes.ASM5) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if ("readFile".equals(name)) {
return classWriter.visitMethod(access, "readFileModify", desc, signature, exceptions);
}
return null;
}
}, ClassReader.EXPAND_FRAMES);
byte[] bytes1 = classWriter.toByteArray();
Files.write(new File("D:\\workspace\\designlecture\\target" +
"\\classes\\com\\learn\\code\\pattern\\visitor\\Example2.class").toPath(), bytes1);
}
这边值得注意的是,先classVisitor进行访问,之后用classWriter进行访问,并返回其方法,这里writer进行访问之后才修改有效。这边writer与常规写法不同,不用set add或writer方法,而是用visit,访问后修改即可。
大家可以使用ams干很多事情。具体业务具体分析使用。比方说类的字节码修改(当然去掉空格不算),这样就可以比对相应源码的不同。
总结
访问者模式是行为模式中最复杂的一种设计模式,一般业务场景也很难用到它。
其主要优点如下。
扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。
访问者(Visitor)模式的主要缺点如下。
增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类
参考和感谢
感谢源码阅读网的鲁班大叔,讲解的访问者模式。
参考网址:http://c.biancheng.net/view/1397.html