定义
- 数据结构与处理被分离开来。
- 编写一个表示“访问者”的类来访问数据结构中的元素,并把对各元素的处理交给访问者类。这样,当需要增加新的处理时,我们只需要编写新的访问者,然后让数据结构可以接受访问者的访问即可。
Visitor模式中登场角色
- Visitor(访问者)
Visitor角色负责对数据结构中每个具体元素(ConcreteElement角色)声明一个用于访问XXXXX的visit(XXXXX)方法。visit(XXXXX)是用于处理XXXXX的方法,负责实现该方法的是ConcreteVisitor角色。
- ConcreteVisitor(具体的访问者)
ConcreteVisitor角色负责实现Visitor角色所定义的接口(API)。它要实现所有的visit(XXXXX)方法,即实现如何处理每个ConcreteElement角色。
- Element(元素)
Element角色表示Visitor角色的访问对象。它声明了接受访问者的accept方法。accept方法接收到的参数是Visitor角色。
- ConcreteElement
ConcreteElement角色负责实现Element角色所定义的接口(API)。
- ObjectStructure(对象结构)
ObjectStructure角色负责处理Element角色的集合。ConcreteVisitor角色为每个Element角色都准备了处理方法。
Visitor模式的类图
拓展思路的要点
- 双重分发
整理一下Visitor模式中方法的调用关系:
accept(接受)方法的调用方式如下:
element.accept(visitor);
而visit(访问)方法的调用凡是如下:
visitor.visit(element);
对比一下这两个方法会发现,它们是相反的关系。element接受visitor,而visitor又访问element。
在Visitor模式中,ConcreteElement和Concrete这两个角色共同决定了实际进行的处理。这种消息分发的方法一般被称为双重分发(double dispatch)。
- 为什么要弄得这么复杂
Visitor模式的目的是将处理从数据结构中分离出来。数据结构很重要,它能将元素集合和关联在一起。但是,需要注意的是,【保存数据结构】与【以数据结构为基础进行处理】是两种不同的东西。
Visitor模式提高了ConcreteElement角色作为组件的独立性。如果将进行处理的方法定义在ConcreteElement角色中,当每次要扩展功能,增加新的“处理”时,就不得不去修改ConcreteElement角色类。
- 开闭原则——对扩展开放,对修改关闭
第一:对扩展是开放的。在设计类时,若无特殊理由,必须要考虑到将来可能会扩展类。绝不能毫无理由地禁止扩展类。这就是“对扩展是开放的”的意思。
第二:对修改是关闭的。但是,如果在每次扩展类时都需要修改现有的类就太麻烦了。所以我们需要在不用修改现有类的前提下能够扩展类,这就是“对修改是关闭的”意思。
功能需求总是在不断变化,而且这些功能需求大都是“希望扩展某个功能”。因此,如果不能比较容易地扩展类,开发过程将会变得非常困难。另一方面,如果要修改已经编写和测试完成的类,又可能会导致软件产品质量降低。
对扩展开放、对修改关闭的类具有高可复用性,可作为组件复用。设计模式和面向对象的目的正是为我们提供一种结构,可以帮助我们设计出这样的类。
- 易于增加ConcreteVisitor角色
使用Visitor模式可以很容易地增加ConcreteVisitor角色。因为具体的处理被交给ConcreteVisitor角色负责,因此完全不用修改ConcreteElement角色。
- 难以增加ConcreteElement角色
虽然使用Visitor模式可以很容易地增加ConcreteVisitor角色,不过它却很难应对ConcreteElement角色的增加。
- Visitor工作所需的条件
“在Visitor模式中,对数据结构中的元素进行处理的任务被分离出来,交给Visitor类负责。这样,就实现类数据结构与处理的分离”这个主题,要达到这个目的是有条件的,那就是Element角色必须向Visitor角色公开足够多的信息。
访问者只有从数据结构中获取了足够多的信息后才能工作。如果无法获取到这些信息,它就无法工作。这样做的缺点是,如果公开了不应当被公开的信息,将来对数据结构的改良就会变得非常困难。
相关的设计模式
Iterator模式和Visitor模式都是在某种数据结构上进行处理。
Iterator模式用于逐个遍历保存在数据结构中的元素。
Visitor模式用于对保存在数据结构中的元素进行某种特定的处理。
有时访问者所访问的数据结构会使用Composite模式。
- Interpreter模式(第23章)
在Interpreter模式中,有时会使用Visitor模式。例如,在生成了语法树后,可能会使用Visitor模式访问语法树的各个节点进行处理。
代码
- Visitor(访问者)
Visitor类是表示访问者的抽象类。访问者依赖于它所访问的数据结构。从外部调用visit方法时,程序会根据接收的参数的类型自动选择和执行相应的visit方法。这种方式称为方法的重载。
public abstract class Visitor {
public abstract void visit(File file);
public abstract void visit(Directory directory);
}
- Element(元素)
Visitor类是表示访问者的类,而Element接口则是接受访问者的访问的接口。如果将Visitor比喻为玛利亚,Element接口就相当于住宿的地方(实现了Element接口的类的实例才是实际住宿的地方)。
Element接口中声明了accept方法。该方法的参数是访问者Visitor类。
public interface Element {
public abstract void accept(Visitor v);
}
public abstract class Entry implements Element {
protected Entry parent; // 父节点
public abstract String getName(); // 获取名字
public abstract int getSize(); // 获取大小
public Entry add(Entry entry) throws FileTreatMentException { // 增加目录条目
throw new FileTreatMentException();
}
public Iterator<Entry> iterator() throws FileTreatMentException { // 生成Iterator
throw new FileTreatMentException();
}
@Override
public String toString() { // Template模式
return getName() + " (" + getSize() + ")";
}
}
- ConcreteElement
public class File extends Entry {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
@Override
public String getName() {
return name;
}
@Override
public int getSize() {
return size;
}
@Override
public void accept(Visitor v) {
v.visit(this);
}
}
public class Directory extends Entry {
private String name; // 文件夹的名字
private List<Entry> dir = new ArrayList<Entry>(); // 文件夹中目录条目的集合
public Directory(String name) { // 构造函数
this.name = name;
}
@Override
public String getName() { // 获取名字
return name;
}
@Override
public int getSize() {
SizeVisitor sizeVisitor = new SizeVisitor();
accept(sizeVisitor);
return sizeVisitor.getSize();
}
public Entry add(Entry entry) { // 增加目录条目
dir.add(entry);
entry.parent = this; // 加入文件夹时,记录父节点
return this;
}
@Override
public Iterator<Entry> iterator() throws FileTreatMentException {
return dir.iterator();
}
@Override
public void accept(Visitor v) {
v.visit(this);
}
}
- ConcreteVisitor(具体的访问者)
public class ListVisitor extends Visitor {
private String currentdir = ""; // 当前访问的文件夹的名字
@Override
public void visit(File file) { // 在访问文件时被调用
System.out.println(currentdir + "/" + file);
}
@Override
public void visit(Directory directory) { // 在访问文件夹时被调用
System.out.println(currentdir + "/" + directory);
String savedir = currentdir;
currentdir = currentdir + "/" + directory.getName();
Iterator<Entry> it = directory.iterator();
while (it.hasNext()) {
Entry entry = it.next();
entry.accept(this);
}
currentdir = savedir;
}
}
- 异常类
public class FileTreatMentException extends RuntimeException {
private static final long serialVersionUID = 1L;
public FileTreatMentException() {
}
public FileTreatMentException(String message) {
super(message);
}
}
- Main类
public class Main {
public static void main(String[] args) {
System.out.println("Making root entries...");
Directory rootdir = new Directory("root");
Directory bindir = new Directory("bin");
Directory tmpdir = new Directory("tmp");
Directory usrdir = new Directory("usr");
rootdir.add(bindir);
rootdir.add(tmpdir);
rootdir.add(usrdir);
bindir.add(new File("vi", 10000));
bindir.add(new File("latex", 20000));
rootdir.accept(new ListVisitor());
System.out.println("");
System.out.println("Making user entries...");
Directory yuki = new Directory("yuki");
Directory hanako = new Directory("hanako");
Directory tomura = new Directory("tomura");
usrdir.add(yuki);
usrdir.add(hanako);
usrdir.add(tomura);
yuki.add(new File("diary.html", 100));
yuki.add(new File("Composite.java", 200));
hanako.add(new File("memo.tex", 300));
tomura.add(new File("game.doc", 400));
tomura.add(new File("junk.mail", 500));
rootdir.accept(new ListVisitor());
}
}
注:博客中的图片来自网上。