Visitor模式可以用来把数据结构与处理分离开。通俗来说就是编写一个访问者类来访问数据结构中的元素,并把对各元素的处理交给访问者类。这样,当需要增加新的处理时,只需要编写新的访问者,然后让数据结构可以接受访问者的访问即可。
下面先看示例程序的类图。
在示例程序中,使用Composite模式中用到了那个文件和文件夹的例子作为访问者要访问的数据结构。访问者会访问由文件和文件夹构成的数据结构,然后显示出文件和文件夹的一览。
1 package bigjunoba.bjtu.visitor; 2 3 public abstract class Visitor { 4 public abstract void visit(File file); 5 public abstract void visit(Directory directory); 6 }
Visitor类是表示访问者的抽象类。访问者依赖于它所访问的数据结构(即File类和Directory类)。这里visit方法用到了重载,分别表示了用于访问文件和文件夹的方法。
1 package bigjunoba.bjtu.visitor; 2 3 public interface Element { 4 public abstract void accept(Visitor v); 5 }
Element接口是接受访问者的访问的接口。accept方法的参数是访问者Visitor类。
1 package bigjunoba.bjtu.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 Entry add(Entry entry) throws FileTreatmentException { // 增加目录条目 9 throw new FileTreatmentException(); 10 } 11 public Iterator<Element> iterator() throws FileTreatmentException { // 生成Iterator 12 throw new FileTreatmentException(); 13 } 14 public String toString() { // 显示字符串 15 return getName() + " (" + getSize() + ")"; 16 } 17 }
Entry类与Composite模式中的Entry类是一样的,不过该Entry类实现了Element接口,这是为了让Entry类适用于Visitor模式。实际上实现Element接口中声明的抽象方法accept的是Entry类的子类。
add方法仅对Directory类有效,因此在Entry类中,让它简单地报错。同样,用于获取Iterator的iterator方法也仅对Directory类有效,也让它简单地报错。
1 package bigjunoba.bjtu.visitor; 2 3 public class File extends Entry { 4 private String name; 5 private int size; 6 public File(String name, int size) { 7 this.name = name; 8 this.size = size; 9 } 10 public String getName() { 11 return name; 12 } 13 public int getSize() { 14 return size; 15 } 16 public void accept(Visitor v) { 17 v.visit(this); 18 } 19 }
File类中要注意的是它是如何实现accept接口的。accept方法的参数是Visitor类,然后调用Visitor类的visit方法,由于这里的this是File类的实例,所以调用的是第一个visit方法。
1 package bigjunoba.bjtu.visitor; 2 3 import java.util.Iterator; 4 import java.util.ArrayList; 5 6 public class Directory extends Entry { 7 private String name; // 文件夹名字 8 private ArrayList<Entry> dir = new ArrayList<Entry>(); // 目录条目集合 9 public Directory(String name) { // 构造函数 10 this.name = name; 11 } 12 public String getName() { // 获取名字 13 return name; 14 } 15 public int getSize() { // 获取大小 16 int size = 0; 17 Iterator<Entry> it = dir.iterator(); 18 while (it.hasNext()) { 19 Entry entry = (Entry)it.next(); 20 size += entry.getSize(); 21 } 22 return size; 23 } 24 public Entry add(Entry entry) { // 增加目录条目 25 dir.add(entry); 26 return this; 27 } 28 public Iterator iterator() { // 生成Iterator 29 return dir.iterator(); 30 } 31 public void accept(Visitor v) { // 接受访问者的访问 32 v.visit(this); 33 } 34 }
同File类一样,调用visit的第一个方法,告诉访问者“当前正在访问的是Directory类的实例”。
1 package bigjunoba.bjtu.visitor; 2 3 import java.util.Iterator; 4 5 public class ListVisitor extends Visitor { 6 private String currentdir = ""; // 当前访问的文件夹的名字 7 public void visit(File file) { // 在访问文件时被调用 8 System.out.println(currentdir + "/" + file); 9 } 10 public void visit(Directory directory) { // 在访问文件夹时被调用 11 System.out.println(currentdir + "/" + directory); 12 String savedir = currentdir; 13 currentdir = currentdir + "/" + directory.getName(); 14 Iterator it = directory.iterator(); 15 while (it.hasNext()) { 16 Entry entry = (Entry)it.next(); 17 entry.accept(this); 18 } 19 currentdir = savedir; 20 } 21 }
ListVisitor类是访问数据结构并显示元素一览。visit(Directory directory)方法,遍历文件夹中的所有目录条目并调用它们各自的accept方法,然后accept方法调用visit方法,visit方法又会调用accept方法,这样就形成了非常复杂的递归调用。通常的递归调用是某个方法调用自身,在Visitor模式中,则是accept方法与visit方法之间相互调用。
1 package bigjunoba.bjtu.visitor; 2 3 public class FileTreatmentException extends RuntimeException { 4 public FileTreatmentException() { 5 } 6 public FileTreatmentException(String msg) { 7 super(msg); 8 } 9 }
处理异常类。
1 package bigjunoba.bjtu.visitor; 2 3 public class Main { 4 public static void main(String[] args) { 5 try { 6 System.out.println("Making root entries..."); 7 Directory rootdir = new Directory("root"); 8 Directory bindir = new Directory("bin"); 9 Directory tmpdir = new Directory("tmp"); 10 Directory usrdir = new Directory("usr"); 11 rootdir.add(bindir); 12 rootdir.add(tmpdir); 13 rootdir.add(usrdir); 14 bindir.add(new File("vi", 10000)); 15 bindir.add(new File("latex", 20000)); 16 rootdir.accept(new ListVisitor()); 17 18 System.out.println(""); 19 System.out.println("Making user entries..."); 20 Directory yuki = new Directory("yuki"); 21 Directory hanako = new Directory("hanako"); 22 Directory tomura = new Directory("tomura"); 23 usrdir.add(yuki); 24 usrdir.add(hanako); 25 usrdir.add(tomura); 26 yuki.add(new File("diary.html", 100)); 27 yuki.add(new File("Composite.java", 200)); 28 hanako.add(new File("memo.tex", 300)); 29 tomura.add(new File("game.doc", 400)); 30 tomura.add(new File("junk.mail", 500)); 31 rootdir.accept(new ListVisitor()); 32 } catch (FileTreatmentException e) { 33 e.printStackTrace(); 34 } 35 } 36 }
这里和Composite模式做比较,Composite模式中调用printList方法来显示文件夹中的内容,该方法已经在Directory类(即表示数据结构的类)中实现了。而在Visitor模式中是在访问者中显示文件夹中的内容。这是因为显示文件夹中的内容业数据对数据结构中的各元素进行的处理。
Making root entries... /root (30000) /root/bin (30000) /root/bin/vi (10000) /root/bin/latex (20000) /root/tmp (0) /root/usr (0) Making user entries... /root (31500) /root/bin (30000) /root/bin/vi (10000) /root/bin/latex (20000) /root/tmp (0) /root/usr (1500) /root/usr/yuki (300) /root/usr/yuki/diary.html (100) /root/usr/yuki/Composite.java (200) /root/usr/hanako (300) /root/usr/hanako/memo.tex (300) /root/usr/tomura (900) /root/usr/tomura/game.doc (400) /root/usr/tomura/junk.mail (500)
运行结构和Composite模式中的运行结果一样。