设计模式:访问者(Visitor)模式
设计模式:访问者(Visitor)模式
一、前言
什么叫做访问,如果大家学过数据结构,对于这点就很清晰了,遍历就是访问的一般形式,单独读取一个元素进行相应的处理也叫作访问,读取到想要查看的内容+对其进行处理就叫做访问,那么我们平常是怎么访问的,基本上就是直接拿着需要访问的地址(引用)来读写内存就可以了。
为什么还要有一个访问者模式呢,这就要放到OOP之中了,在面向对象编程的思想中,我们使用类来组织属性,以及对属性的操作,那么我们理所当然的将访问操作放到了类的内部,这样看起来没问题,但是当我们想要使用另一种遍历方式要怎么办呢,我们必须将这个类进行修改,这在设计模式中是大忌,在设计模式中就要保证,对扩展开放,对修改关闭的开闭原则。
因此,我们思考,可不可以将访问操作独立出来变成一个新的类,当我们需要增加访问操作的时候,直接增加新的类,原来的代码不需要任何的改变,如果可以这样做,那么我们的程序就是好的程序,因为可以扩展,符合开闭原则。而访问者模式就是实现这个的,使得使用不同的访问方式都可以对某些元素进行访问。
二、代码
Element 接口:
1 package zyr.dp.visitor; 2 3 public interface Element { 4 5 public abstract void accept(Visitor visitor); 6 7 }
Entry 类:
1 package zyr.dp.visitor; 2 3 import java.util.Iterator; 4 5 public abstract class Entry implements Element{ 6 public abstract String getName(); 7 public abstract int getSize(); 8 public abstract void printList(String prefix); 9 public void printList(){ 10 printList(""); 11 } 12 public Entry add(Entry entry) throws RuntimeException{ 13 throw new RuntimeException(); 14 } 15 public Iterator iterator() throws RuntimeException{ 16 throw new RuntimeException(); 17 } 18 public String toString(){ 19 return getName()+"<"+getSize()+">"; 20 } 21 }
File 类:
1 package zyr.dp.visitor; 2 3 public class File extends Entry { 4 5 private String name; 6 private int size; 7 public File(String name,int size){ 8 this.name=name; 9 this.size=size; 10 } 11 public String getName() { 12 return name; 13 } 14 15 public int getSize() { 16 return size; 17 } 18 19 public void printList(String prefix) { 20 System.out.println(prefix+"/"+this); 21 } 22 public void accept(Visitor visitor) { 23 // System.out.println("开始访问文件:"+this); 24 visitor.visit(this); 25 // System.out.println("结束访问文件:"+this); 26 // System.out.println(); 27 } 28 29 }
Directory类:
1 package zyr.dp.visitor; 2 3 import java.util.ArrayList; 4 import java.util.Iterator; 5 6 public class Directory extends Entry { 7 8 String name; 9 ArrayList entrys=new ArrayList(); 10 public Directory(String name){ 11 this.name=name; 12 } 13 public String getName() { 14 return name; 15 } 16 17 public int getSize() { 18 int size=0; 19 Iterator it=entrys.iterator(); 20 while(it.hasNext()){ 21 size+=((Entry)it.next()).getSize(); 22 } 23 return size; 24 } 25 26 public Entry add(Entry entry) { 27 entrys.add(entry); 28 return this; 29 } 30 31 public Iterator iterator() { 32 return entrys.iterator(); 33 } 34 35 public void printList(String prefix) { 36 System.out.println(prefix+"/"+this); 37 Iterator it=entrys.iterator(); 38 Entry entry; 39 while(it.hasNext()){ 40 entry=(Entry)it.next(); 41 entry.printList(prefix+"/"+name); 42 } 43 } 44 public void accept(Visitor visitor) { 45 // System.out.println("开始访问文件夹:"+this); 46 visitor.visit(this); 47 // System.out.println("结束访问文件夹:"+this); 48 // System.out.println(); 49 } 50 51 }
Visitor 类:
1 package zyr.dp.visitor; 2 3 public abstract class Visitor { 4 5 public abstract void visit(File file); 6 public abstract void visit(Directory directory); 7 8 }
ListVisitor类:
1 package zyr.dp.visitor; 2 3 import java.util.Iterator; 4 5 public class ListVisitor extends Visitor { 6 7 String currentDir = ""; 8 public void visit(File file) { 9 System.out.println(currentDir+"/"+file); 10 } 11 12 public void visit(Directory directory) { 13 System.out.println(currentDir+"/"+directory); 14 String saveDir=currentDir; 15 currentDir+=("/"+directory.getName()); 16 Iterator it=directory.iterator(); 17 while(it.hasNext()){ 18 Entry entry=(Entry)it.next(); 19 entry.accept(this); 20 } 21 currentDir=saveDir; 22 } 23 24 }
FileVisitor 类:
1 package zyr.dp.visitor; 2 3 import java.util.ArrayList; 4 import java.util.Iterator; 5 6 public class FileVisitor extends Visitor { 7 8 String currentDir = ""; 9 String suffix; 10 ArrayList files=new ArrayList(); 11 12 public FileVisitor(String suffix){ 13 this.suffix = suffix; 14 } 15 16 public void visit(File file) { 17 if(file.getName().endsWith(suffix)){ 18 // System.out.println(currentDir+"/"+file); 19 files.add(currentDir+"/"+file); 20 } 21 } 22 23 public void visit(Directory directory) { 24 String saveDir=currentDir; 25 currentDir+=("/"+directory.getName()); 26 Iterator it=directory.iterator(); 27 while(it.hasNext()){ 28 Entry entry=(Entry)it.next(); 29 entry.accept(this); 30 } 31 currentDir=saveDir; 32 } 33 Iterator getFiles(){ 34 return files.iterator(); 35 } 36 37 }
Main类:
1 package zyr.dp.visitor; 2 3 import java.util.Iterator; 4 5 6 public class Main { 7 8 public static void main(String[] args) { 9 10 Directory root=new Directory("根目录"); 11 12 Directory life=new Directory("我的生活"); 13 File eat=new File("吃火锅.txt",100); 14 File sleep=new File("睡觉.html",100); 15 File study=new File("学习.txt",100); 16 life.add(eat); 17 life.add(sleep); 18 life.add(study); 19 20 Directory work=new Directory("我的工作"); 21 File write=new File("写博客.doc",200); 22 File paper=new File("写论文.html",200); 23 File homework=new File("写家庭作业.docx",200); 24 work.add(write); 25 work.add(paper); 26 work.add(homework); 27 28 Directory relax=new Directory("我的休闲"); 29 File music=new File("听听音乐.js",200); 30 File walk=new File("出去转转.psd",200); 31 relax.add(music); 32 relax.add(walk); 33 34 Directory read=new Directory("我的阅读"); 35 File book=new File("学习书籍.psd",200); 36 File novel=new File("娱乐小说.txt",200); 37 read.add(book); 38 read.add(novel); 39 40 root.add(life); 41 root.add(work); 42 root.add(relax); 43 root.add(read); 44 45 root.accept(new ListVisitor()); 46 System.out.println("========================"); 47 FileVisitor visitor=new FileVisitor(".psd"); 48 root.accept(visitor); 49 Iterator it = visitor.getFiles(); 50 while(it.hasNext()){ 51 System.out.println(it.next()); 52 } 53 54 } 55 56 }
运行结果:
可以看到我们的运行结果第一个和使用Composite模式的结果一样,第二个是实现另一种方式的访问,只访问文件后缀为某一特定的内容的文件,结果也是正确的,并且为了说明我们的访问还可以保存下来访问的结果,我们使用了ArrayList自带的迭代器将保存到ArrayList中的结果输出出来,我们当然也可以直接在遍历的时候就输出出来,这个看我们的使用要求了。由此可以看到在保证数据结构(File和Directory)不发生变化的情况下(没有新增或者删除),可以非常方便增加新的一种访问方法,只需要新增加一个访问类即可,但是如果我们数据结构发生变化之后,就需要修改继承自Visitor类的所有类了,这也违背了开闭原则,因此我们应该认真考虑,到底我们的数据结构是定死的还是经常变化的。没有任何一种设计模式是十全十美的,总是有所取舍,有所利弊,根据实际情况来选择才是最好的设计方法。
这里要说明一下双重分发机制,我们来看一下最核心的遍历逻辑,结合组合模式的时候我们已经分析过的遍历方法,递归,大家觉得这次我们要怎么在数据结构外面进行遍历,肯定还是要使用递归了,可是数据结构中的数据在类的内部,怎么递归到内部呢,我们想到了间接递归,也就是双重分发。
1 public void printList(String prefix) { 2 System.out.println(prefix+"/"+this); 3 Iterator it=entrys.iterator(); 4 Entry entry; 5 while(it.hasNext()){ 6 entry=(Entry)it.next(); 7 entry.printList(prefix+"/"+name); 8 } 9 }
上面的代码是在组合模式类的内部遍历的过程,可以明确的看到递归(直接递归)的使用。我们看一下访问者模式中的间接递归:
Directory类中:
public void accept(Visitor visitor) { //System.out.println("开始访问文件夹:"+this); visitor.visit(this); //System.out.println("结束访问文件夹:"+this); //System.out.println(); }
File类:
1 public void accept(Visitor visitor) { 2 //System.out.println("开始访问文件:"+this); 3 visitor.visit(this); 4 //System.out.println("结束访问文件:"+this); 5 //System.out.println(); 6 }
在ListVisitor中:
1 public void visit(File file) { 2 System.out.println(currentDir+"/"+file); 3 } 4 5 public void visit(Directory directory) { 6 System.out.println(currentDir+"/"+directory); 7 String saveDir=currentDir; 8 currentDir+=("/"+directory.getName()); 9 Iterator it=directory.iterator(); 10 while(it.hasNext()){ 11 Entry entry=(Entry)it.next(); 12 entry.accept(this); 13 } 14 currentDir=saveDir; 15 }
我们看到了entry.accept(this)这句话,这句话是非常重要的,我们在Main中是这样用的:
1 root.accept(new ListVisitor());
那么串连起来,在Main中我们通过Directory或者File类型的对象调用accept(访问者)方法,接受访问者的访问,这是访问者和被访问者的第一次亲密接触,亲近对方就是为了获得对方的数据,然后才能对对方的数据进行使用,那么怎么拿到的呢?!我们看到了这句visitor.visit(this);这句话无疑是重要的,被调用者告诉访问者,我将我的内容this,全部给你了,以后访问者就可以对this所指代的被访问者的内容进行操作了,分为两类,如果被访问者是File文件类型的,就会直接输出内容,到达叶子结点,访问结束;如果是文件夹,那就非常有意思了,首先我们仍旧是让被访问者将自己的内容交给访问者visitor.visit(this);,之后public void visit(Directory directory)被调用,通过遍历的方式将属于这个文件夹下面的数据全部拿到Iterator it=directory.iterator();,然后开始一个个的处理,怎么处理呢,继续访问属于这个文件夹下面对象的accept()方法使用entry.accept(this);,来将访问者交过去,交给谁?!肯定是给entry所指的对象,也就是文件夹里面的子文件夹或者文件,如果是文件的话,继续在自己的方法中调用visitor.visit(this);,最终落实到调用 public void visit(File file)通过System.out.println(currentDir+"/"+file);访问结束,如果不是文件呢?若为文件夹,则继续调用属于文件夹的方法,就这样不断地往下面查找,一直到遍历完文件夹下面的所有的元素,因此也是深度优先遍历。就这样通过压栈和出栈,我们完成了最终的遍历,最终的出口有两个,一个是访问文件,输出之后结束,另一个是遍历完文件夹,即使文件夹下面没有文件依旧结束。
1 root.accept(new ListVisitor());
1 public void accept(Visitor visitor) { 2 visitor.visit(this); 3 }
1 public void visit(File file) { 2 System.out.println(currentDir+"/"+file); 3 } 4 5 public void visit(Directory directory) { 6 System.out.println(currentDir+"/"+directory); 7 String saveDir=currentDir; 8 currentDir+=("/"+directory.getName()); 9 Iterator it=directory.iterator(); 10 while(it.hasNext()){ 11 Entry entry=(Entry)it.next(); 12 entry.accept(this); 13 } 14 currentDir=saveDir; 15 }
在accept函数中调用visit,同样在visit中调用accept,这就是间接递归,或者叫做双重分发。产生的原因就是访问者需要和被访问者相互交流,才能一步步的得到想要的数据。我们可以考虑主持人采访一个明星,那么这个明星接受采访,把自己基本信息(能问的问题以及某些答案)告诉主持人,问主持人有问题吗?如果主持人有问题(还能向下问)要问那么就再次拿着新的问题问这个明星,这个明星再次将自己关于这方面的信息告诉主持人;如果没有问题(得到答案),主持人将信息总结之后说出来。就这样一直持续下去,直到主持人没问题问了,并且明星的信息也都被问到了,这样采访就结束了。由此可见,很多时候设计模式都是和生活密切相关的,生活中的常识有时候就是一些套路,而这种套路就是一种抽象的模式。
三、总结
访问者模式是一个非常有意思的模式,因为自己需要得到数据就需要向被访者索取,如果能够一次索取成功,访问就结束了,如果还需要其他信息,则再次向被访问者索取,就这样知道拿到自己需要的所有数据。在本例中借用了组合模式中的数据结构,那是因为这种树形的结构很适合我们进行递归访问。访问者模式和迭代器模式都是在某种数据结构上进行处理,一种是对数据结构中的元素进行某种特定的处理,另一种是用某种方式遍历所有元素。在实际应用中,我们根据实际需要来考虑是不是需要双重分发机制。在本例中的访问者模式中用到了组合模式、委托(组合)、双重分发等原理,便于新增访问方式,不便于对数据结构的修改。