在数据结构中保存着许多元素,需要对这些元素进行“处理”,这时,“处理”代码一般放在表示数据结构的类中。但是,如果“处理”有很多种呢?这种情况下,每当增加一种处理,就不得不去修改表示数据结构的类。
在访问者模式中,数据结构与处理被分离开。编写一个表示“访问者”的类来访问数据结构中的元素,并把各元素的处理交给访问者类。这样,当需要增加新的处理时,只需要编写新的访问者,然后让数据结构接受访问者的访问即可。

示例程序:

名字 说明
Visitor 表示访问者的抽象类,它访问文件和文件夹
Element 表示数据结构的接口,它接受访问者的访问
ListVisitor Visitor的子类,显示文件和文件夹一览
Entry File类和Directory类的父类,它是抽象类
(实现了Element接口)
File 表示文件的类
Directory 表示文件夹的类
FileTreatmentException 表示向文件中add时发生的异常
Main 测试程序的类

Visitor类:

表示访问者的抽象类。访问者依赖于它所访问的数据结构(File和Directory类)。
类中定义了两个方法,类名相同,接收的参数不同,一个接收File类型的参数,另一个接收Directory类型的参数。从外部调用Vistor方法时,程序会根据接收的参数类型自动选择和执行相应的visotor方法。通常,我们称这种方式为方法的重载。

  1. public abstract class Visitor {
  2. public abstract void visit(File file);
  3. public abstract void visit(Directory directory);
  4. }

Element接口:

该接口是接受访问者的访问的接口。文件类通过实现该接口来实现能被访问的效果。

  1. public interface Element {
  2. public abstract void accept(Visitor v);
  3. }

Entry类:

将add方法和iterator方法实现在该接口中,采用直接报错的形式。后续Directory重写该方法来满足需求。

  1. public abstract class Entry implements Element{
  2. public abstract String getName();
  3. public abstract int getSize();
  4. public Entry add(Entry entry) throws FileTreatMentException{
  5. throw new FileTreatMentException();
  6. }
  7. public Iterator iterator() throws FileTreatMentException{
  8. throw new FileTreatMentException();
  9. }
  10. public String toString(){
  11. return getName()+"("+getSize()+")";
  12. }
  13. }

File类:

需要注意的就是accept方法,重写来接受访问者的访问。

  1. public class File extends Entry{
  2. private String name;
  3. private int size;
  4. public File(String name, int size) {
  5. this.name = name;
  6. this.size = size;
  7. }
  8. @Override
  9. public void accept(Visitor v) {
  10. v.visit(this);
  11. }
  12. @Override
  13. public String getName() {
  14. return name;
  15. }
  16. @Override
  17. public int getSize() {
  18. return size;
  19. }
  20. }

Directory类:

重写iterator方法和accept方法。

  1. public class Directory extends Entry{
  2. private String name;
  3. private ArrayList dir=new ArrayList();
  4. public Directory(String name) {
  5. this.name = name;
  6. }
  7. @Override
  8. public Entry add(Entry entry) throws FileTreatMentException {
  9. dir.add(entry);
  10. return this;
  11. }
  12. @Override
  13. public Iterator iterator() throws FileTreatMentException {
  14. return dir.iterator();
  15. }
  16. @Override
  17. public void accept(Visitor v) {
  18. v.visit(this);
  19. }
  20. @Override
  21. public String getName() {
  22. return name;
  23. }
  24. @Override
  25. public int getSize() {
  26. int size=0;
  27. Iterator iterator = dir.iterator();
  28. while (iterator.hasNext()){
  29. Entry entry = (Entry)iterator.next();
  30. size+=entry.getSize();
  31. }
  32. return size;
  33. }
  34. }

ListVisitor类:

对抽象类中的抽象方法进行实现,来达到访问不同类别文件的效果。

  1. public class ListVisitor extends Visitor{
  2. private String currentDir = "";
  3. @Override
  4. public void visit(File file) {
  5. System.out.println(currentDir+"/"+file);
  6. }
  7. @Override
  8. public void visit(Directory directory) {
  9. System.out.println(currentDir+"/"+directory);
  10. String savaDir = currentDir;
  11. currentDir= currentDir + "/" + directory.getName();
  12. Iterator iterator = directory.iterator();
  13. while (iterator.hasNext()){
  14. Entry next = (Entry) iterator.next();
  15. next.accept(this);
  16. }
  17. currentDir=savaDir;
  18. }
  19. }

FileTreatmentException类:

  1. public class FileTreatmentException extends RuntimeException{
  2. public FileTreatmentException() {
  3. }
  4. public FileTreatmentException(String msg) {
  5. super(msg);
  6. }
  7. }

Main类:

  1. public class Main {
  2. public static void main(String[] args) {
  3. System.out.println("创建root目录");
  4. Directory root = new Directory("root");
  5. Directory bin = new Directory("bin");
  6. Directory tmp = new Directory("tmp");
  7. Directory usr = new Directory("usr");
  8. root.add(bin);
  9. root.add(tmp);
  10. root.add(usr);
  11. bin.add(new File("vi",10000));
  12. bin.add(new File("latex",20000));
  13. root.accept(new ListVisitor());
  14. System.out.println("");
  15. System.out.println("创建user目录");
  16. Directory yuki = new Directory("yuki");
  17. Directory hanako = new Directory("hanako");
  18. Directory tomura = new Directory("tomura");
  19. usr.add(yuki);
  20. usr.add(hanako);
  21. usr.add(tomura);
  22. yuki.add(new File("a.html",100));
  23. yuki.add(new File("b.java",200));
  24. hanako.add(new File("memo.txt",300));
  25. tomura.add(new File("game.doc",400));
  26. root.accept(new ListVisitor());
  27. }
  28. }

Visitor与Element之间的相互调用:

  1. 首先,Main生产了ListVisitor的实例。还生成了其他的Directory类和File类的实例;
  2. 接着,Main类调用Directory类的accept方法。这时传递的参数是ListVisitor的实例;
  3. Directory类的实例调用接收到的参数ListVisitor方法中的Visitor(Directory)方法;
  4. 接下来,ListVisitor类的实例会访问文件夹,并调用找到的第一个文件的accept方法,传递的参数是自身;
  5. File的实例调用接收到的参数ListVistor的visit(File)方法。一直在递归调用。
  6. 从visit返回到accept,接着又从accept也返回出来,然后调用另外一个File的实例的accept方法,传递的参数是ListVisitor的实例this。
  7. 与前面一样,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:

    负责实现Element角色所定义的接口。

    ObjectStructure(对象结构):

    负责处理Element角色的集合。ConcreteElement为每个Element角色都准备了处理方法,在Directory中实现了iterator方法。

    拓展思路的要点:

    双重分发:

    Visitor模式方法的调用关系。
    accept方法的调用方法如下:

    1. public void accept(Visitor v) {
    2. v.visit(this);
    3. }

    而Visit方法的调用方式如下:

    1. public void visit(File file) {
    2. System.out.println(currentDir+"/"+file);
    3. }

    对比一下两个方法可以发现,它们是相反的关系。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模式访问语法树的各个节点进行处理。