在数据结构中保存着许多元素,需要对这些元素进行“处理”,这时,“处理”代码一般放在表示数据结构的类中。但是,如果“处理”有很多种呢?这种情况下,每当增加一种处理,就不得不去修改表示数据结构的类。
在访问者模式中,数据结构与处理被分离开。编写一个表示“访问者”的类来访问数据结构中的元素,并把各元素的处理交给访问者类。这样,当需要增加新的处理时,只需要编写新的访问者,然后让数据结构接受访问者的访问即可。
示例程序:
| 名字 | 说明 |
|---|---|
| Visitor | 表示访问者的抽象类,它访问文件和文件夹 |
| Element | 表示数据结构的接口,它接受访问者的访问 |
| ListVisitor | Visitor的子类,显示文件和文件夹一览 |
| Entry | File类和Directory类的父类,它是抽象类 (实现了Element接口) |
| File | 表示文件的类 |
| Directory | 表示文件夹的类 |
| FileTreatmentException | 表示向文件中add时发生的异常 |
| Main | 测试程序的类 |
Visitor类:
表示访问者的抽象类。访问者依赖于它所访问的数据结构(File和Directory类)。
类中定义了两个方法,类名相同,接收的参数不同,一个接收File类型的参数,另一个接收Directory类型的参数。从外部调用Vistor方法时,程序会根据接收的参数类型自动选择和执行相应的visotor方法。通常,我们称这种方式为方法的重载。
public abstract class Visitor {public abstract void visit(File file);public abstract void visit(Directory directory);}
Element接口:
该接口是接受访问者的访问的接口。文件类通过实现该接口来实现能被访问的效果。
public interface Element {public abstract void accept(Visitor v);}
Entry类:
将add方法和iterator方法实现在该接口中,采用直接报错的形式。后续Directory重写该方法来满足需求。
public abstract class Entry implements Element{public abstract String getName();public abstract int getSize();public Entry add(Entry entry) throws FileTreatMentException{throw new FileTreatMentException();}public Iterator iterator() throws FileTreatMentException{throw new FileTreatMentException();}public String toString(){return getName()+"("+getSize()+")";}}
File类:
需要注意的就是accept方法,重写来接受访问者的访问。
public class File extends Entry{private String name;private int size;public File(String name, int size) {this.name = name;this.size = size;}@Overridepublic void accept(Visitor v) {v.visit(this);}@Overridepublic String getName() {return name;}@Overridepublic int getSize() {return size;}}
Directory类:
重写iterator方法和accept方法。
public class Directory extends Entry{private String name;private ArrayList dir=new ArrayList();public Directory(String name) {this.name = name;}@Overridepublic Entry add(Entry entry) throws FileTreatMentException {dir.add(entry);return this;}@Overridepublic Iterator iterator() throws FileTreatMentException {return dir.iterator();}@Overridepublic void accept(Visitor v) {v.visit(this);}@Overridepublic String getName() {return name;}@Overridepublic int getSize() {int size=0;Iterator iterator = dir.iterator();while (iterator.hasNext()){Entry entry = (Entry)iterator.next();size+=entry.getSize();}return size;}}
ListVisitor类:
对抽象类中的抽象方法进行实现,来达到访问不同类别文件的效果。
public class ListVisitor extends Visitor{private String currentDir = "";@Overridepublic void visit(File file) {System.out.println(currentDir+"/"+file);}@Overridepublic void visit(Directory directory) {System.out.println(currentDir+"/"+directory);String savaDir = currentDir;currentDir= currentDir + "/" + directory.getName();Iterator iterator = directory.iterator();while (iterator.hasNext()){Entry next = (Entry) iterator.next();next.accept(this);}currentDir=savaDir;}}
FileTreatmentException类:
public class FileTreatmentException extends RuntimeException{public FileTreatmentException() {}public FileTreatmentException(String msg) {super(msg);}}
Main类:
public class Main {public static void main(String[] args) {System.out.println("创建root目录");Directory root = new Directory("root");Directory bin = new Directory("bin");Directory tmp = new Directory("tmp");Directory usr = new Directory("usr");root.add(bin);root.add(tmp);root.add(usr);bin.add(new File("vi",10000));bin.add(new File("latex",20000));root.accept(new ListVisitor());System.out.println("");System.out.println("创建user目录");Directory yuki = new Directory("yuki");Directory hanako = new Directory("hanako");Directory tomura = new Directory("tomura");usr.add(yuki);usr.add(hanako);usr.add(tomura);yuki.add(new File("a.html",100));yuki.add(new File("b.java",200));hanako.add(new File("memo.txt",300));tomura.add(new File("game.doc",400));root.accept(new ListVisitor());}}
Visitor与Element之间的相互调用:
- 首先,Main生产了ListVisitor的实例。还生成了其他的Directory类和File类的实例;
- 接着,Main类调用Directory类的accept方法。这时传递的参数是ListVisitor的实例;
- Directory类的实例调用接收到的参数ListVisitor方法中的Visitor(Directory)方法;
- 接下来,ListVisitor类的实例会访问文件夹,并调用找到的第一个文件的accept方法,传递的参数是自身;
- File的实例调用接收到的参数ListVistor的visit(File)方法。一直在递归调用。
- 从visit返回到accept,接着又从accept也返回出来,然后调用另外一个File的实例的accept方法,传递的参数是ListVisitor的实例this。
- 与前面一样,File的实例调用Visit(File)方法,所有的处理完成后,逐步返回,最后回到Main类中的调用accept方法的地方。
- 对于Directory类的实例和File类的实例,调用了它们的accept方法;
- 对于每一个Directory类的实例和File类的实例,只调用了一次它们的accept方法;
- 对于ListVisitor的实例,调用了它的visitor(Directory)和visitor(File)方法;
处理visitor(Directory)和visit(File)的是同一个ListVisitor的实例。
Visitor模式中的登场角色:
Visitor(访问者):
负责对数据结构中每个具体的元素声明一个用于访问的visit方法。
ConcreteVisitor(具体的访问者):
负责实现visitor角色所定义的接口(API)。它要实现所有的visit方法,即实现如何处理每个ConcreteElement角色。由ListVisitor类扮演此角色。
Element(元素):
表示访问者的访问对象,声明了接受访问者的accept方法,accept方法接收到的参数是Visitor角色。
ConcreteElement:
ObjectStructure(对象结构):
负责处理Element角色的集合。ConcreteElement为每个Element角色都准备了处理方法,在Directory中实现了iterator方法。
拓展思路的要点:
双重分发:
Visitor模式方法的调用关系。
accept方法的调用方法如下:public void accept(Visitor v) {v.visit(this);}
而Visit方法的调用方式如下:
public void visit(File file) {System.out.println(currentDir+"/"+file);}
对比一下两个方法可以发现,它们是相反的关系。Element接受Visitor,而Visitor又访问element;这种消息分发的方式一般被称为双重分发。
为什么要弄得这么复杂:
看起来好像访问者模式非常复杂,那为什么要这样做呢?
目的是将处理从数据结构中分离出来。数据结构很重要,它能将元素集合和关联在一起。但是保存数据结构与以数据结构为基础进行处理是两种不同的东西。所以分离开提高了组件的独立性。开闭原则——对扩展开放,对修改关闭:
开闭原则的主张类是这样的:
对扩展是开放的;
- 对修改是关闭的:
- 在设计类时,若无特殊理由,必须要考虑到将来可能会扩展类,决不能毫无理由地禁止扩展类,这就是“对扩展是开放的”的意思。
- 但是如果每次扩展类时都需要修改现有的类就太过于麻烦了。所以需要在不用修改现有类的前提下能够扩展类,这就是“对修改是关闭的”的意思。
- 如果需要修改现有代码就得在不修改现有代码的前提下进行扩展,这就是开闭原则。
- 功能需求总是在不断变化,而且这些功能需求大都是“希望扩展某个功能”。因此,如果不能比较容易地扩展类,开发过程将会变得非常困难。另外,如果要修改意见编写和测试完成的类,又可能会导致软件产品的质量降低。
- 对扩展开放、对修改关闭的类具有高可复用性,可作为组件复用。设计模式和面向对象的目的正是为我们提供一种结构,可以帮助我们设计出这样的类。
易于增加ConcreteVisitor角色:
使用Visitor模式可以很容易地增加ConcreteVisitor角色,因为具体的处理被交给ConcreteVisitor
负责,因此完全不需要修改ConcreteVisitor角色。难以增加ConcreteElement角色:
虽然可以很简单地使用Visitor模式可以很简单地增加ConcreteVisitor,但却很难应对ConcreteElement角色的增加。需要回到之前定义好的所有Visitor类中声明访问这个新角色的方法,并且在Visitor的子类中实现。Visitor工作所需的条件:
“在visitor模式中,对数据结构中的元素进行处理的任务被分离出来,交给Visitor类负责。这样,就实现了数据结构与处理的分离”这个主题。但是要达到这个目的是有条件的,Element角色必须要向Visitor角色公开足够多的信息。
实例中,Visitor方法必须要调用每个目录条目中的accept方法。为此,Directory类必须提供用于获取每个目录条目的iterator方法。
访问者只有从数据结构中获取了足够多的信息才能工作。如果无法获取到这些信息,它就无法工作。这样做的缺点是:如果公开了不该公开的信息,将来对数据结构的改良将会变得非常困难。相关的设计模式:
Iterator模式:
Iterator模式和Visitor模式都是在某种数据结构上进行处理。
Iterator模式用于逐个遍历保存在数据结构中的元素。
Visitor模式用于对保存在数据结构中的元素进行某种特定的处理。Composite模式:
有时访问者所访问的数据结构会使用Composite模式。Interpreter模式:
在Interpreter模式中,有时会使用Visitor模式。例如,在生成了语法树后,可能会使用Visitor模式访问语法树的各个节点进行处理。
