1. Visitor模式简介
Visitor是访问者的意思。
一个数据结构包含多种数据元素,这些数据元素方便存储,却不一定方便使用。因此我们有必要架起一座桥梁,即对数据元素进行处理,方便别人使用。问题来了:数据的处理是放在数据结构类里面还是再造一个类专门用来处理?
如果一个数据结构需要多种处理方式,那每增加一种就要修改数据结构的类,显然不合适。访问者模式就是解决这个问题来的。在这个模式中,数据结构和处理被分离开。我们编写一个访问者,即专门处理数据元素的类来处理数据元素,一旦有新的处理方式,就编写新的访问者类去访问数据结构即可。
2. 示例
接下来的例子以之前的Composite模式为基础编写。为File和Directory类添加一个accept方法,这个方法接收Visitor类,即访问者类。通过这个访问者我们能获得File和Directory类的经过处理的数据展示。
2.1 类图
2.2 代码
ListVisitor
里面的visit(Directory directory)
使用了递归,值得好好体会一下。
public abstract class Visitor {
public abstract void visit(File file);
public abstract void visit(Directory directory);
}
public class ListVisitor extends Visitor {
private String currentdir = "";
public void visit(File file) {
System.out.println(currentdir + "/" + file);
}
public void visit(Directory directory) {
System.out.println(currentdir + "/" + directory);
String savedir = currentdir;
currentdir = currentdir + "/" + directory.getName();
Iterator it = directory.iterator();
while (it.hasNext()) {
Entry entry = (Entry) it.next();
entry.accept(this);
}
currentdir = savedir;
}
}
public interface Element {
public abstract void accept(Visitor v);
}
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() + ")";
}
}
public class File extends Entry {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
public String getName() {
return name;
}
public int getSize() {
return size;
}
public void accept(Visitor v) {
v.visit(this);
}
}
public class Directory extends Entry {
private String name; // 文件夹名字
private ArrayList dir = new ArrayList(); // 目录条目集合
public Directory(String name) { // 构造函数
this.name = name;
}
public String getName() { // 获取名字
return name;
}
public int getSize() { // 获取大小
int size = 0;
Iterator it = dir.iterator();
while (it.hasNext()) {
Entry entry = (Entry) it.next();
size += entry.getSize();
}
return size;
}
public Entry add(Entry entry) { // 增加目录条目
dir.add(entry);
return this;
}
public Iterator iterator() { // 生成Iterator
return dir.iterator();
}
public void accept(Visitor v) { // 接受访问者的访问
v.visit(this);
}
}
public class Main {
public static void main(String[] args) {
try {
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 xiaoming = new Directory("小明");
Directory xiaohong = new Directory("小红");
Directory xiaohua = new Directory("小花");
usrdir.add(xiaoming);
usrdir.add(xiaohong);
usrdir.add(xiaohua);
xiaoming.add(new File("diary.html", 100));
xiaoming.add(new File("Composite.java", 200));
xiaohong.add(new File("memo.tex", 300));
xiaohua.add(new File("game.doc", 400));
xiaohua.add(new File("junk.mail", 500));
rootdir.accept(new ListVisitor());
} catch (FileTreatmentException e) {
e.printStackTrace();
}
}
}
/////////////////////////////结果////////////////////////////////
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/小明 (300)
/root/usr/小明/diary.html (100)
/root/usr/小明/Composite.java (200)
/root/usr/小红 (300)
/root/usr/小红/memo.tex (300)
/root/usr/小花 (900)
/root/usr/小花/game.doc (400)
/root/usr/小花/junk.mail (500)
3. 模式的角色和类图
- Visitor(访问者): 抽象的访问者类,里面声明了供Element调用的访问方法visit。本例中由Visitor类扮演此角色。
- ConcreteVisitor(具体的访问者):具体的访问者类,里面实现了供Element调用的访问方法visit,本例中由ListVisitor类扮演此角色。
- Element(元素):表示Visitor访问的对象,声明了接收访问者的accept方法,参数是Visitor角色,本例中由Element接口扮演此角色。
- ConcreteElement(具体的元素):实现Element定义的接口,本例中由File类和Directory类扮演此角色。
- ObjectStructure(对象结构):负责处理Element角色的集合。ConcreteVisitor角色为每个Element角色都准备了处理方法,在本例中,由Directory类扮演此角色。为了让ConcreteVisitor可以遍历处理每个Element角色,在示例程序中,我们在Directory类中实现了iterator方法。
4. 思路拓展
4.1 双重分发
在代码中,元素接受访问者:element.accept(visitor)
,访问者访问元素visitor.visit(element)
,对比这两个方法,他们是相反的关系,这种消息分发的方式被称为双重分发。
4.2 开闭原则
-
对扩展开放
-
对修改关闭
如果要增加功能,要在不修改现有代码的前提下进行扩展。Visitor模式中,如果要扩展数据结构的访问方法,不需要修改数据结构的类, 只需要增加一个访问者类即可。体现了开闭原则。
4.3 难以增加ConcreteElement角色
Visitor模式很容易增加ConcreteVisitor角色,却很难应对ConcreteElement角色的增加。假设我们要增加Entry类的子类Device类,这个类和File类、Directory类是兄弟关系。我们就需要在Visitor类中声明一个visit(Device)方法,所有Visitor类的子类都要实现这个方法。
4.4 Visitor工作所需的条件
Visitor需要从数据结构中获取到足够多的信息才能够工作。如果数据结构向Visitor公开了不该公开的信息,将来对数据结构的改良就会变得非常困难。