Visitor模式——访问数据结构并处理数据
在数据结构中保存着许多元素,我们会对这些元素进行“处理”。这时,如通常的做法是把“处理”的代码放到类中,但是当“处理”的方式有很多种时,这时,每增加一种处理,就需要去修改表示数据结构的类。
在Visitor模式中,**数据结构与处理被分离开来。我们编写一个表示“访问者”的类来访问数据结构中的数据,并把对各元素的处理交给访问者类。**这样,当需要增加新的处理时,我们只需要编写新的访问者,然后让数据结构可以接受访问者的访问即可。
下面的示例程序使用到了Composite模式中文件和文件夹的递归的数据结构,并编写了两个访问者,一个访问者可以访问由文件和文件夹构成的数据结构,然后显示出文件和文件夹的一览;另一个访问者可以访问由文件和文件夹构成的数据结构,然后过滤出带有指定后缀名的文件。关于Composite模式中文件和文件夹的递归的数据结构,参见链接(设计模式:Composite模式)。
- 类和接口的一览表
名字 | 说明 |
---|---|
Visitor | 表示访问者的抽象类,它访问文件和文件夹 |
Element | 表示数据结构的接口类,它接受访问者的访问 |
Entry | File类和Directory类的父类,它是抽象类(实现了Element接口) |
File | 表示文件的类 |
Directory | 表示文件夹的类 |
FileTreatmentException | 表示对文件调用文件夹的方法时发生异常的类 |
ListVisitor | Visitor类的子类,显示文件和文件夹一览 |
FileFindVisitor | Visitor类的子类,过滤带有指定后缀名的文件 |
Main | 测试程序行为的类 |
- Visitor类
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类
import java.util.Iterator;
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();
}
@Override
public String toString() {
return getName() + "(" + getSize() + ")";
}
}
- File类
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);
}
}
- Directory类
import java.util.ArrayList;
import java.util.Iterator;
public class Directory extends Entry {
private String name;
private ArrayList dir = new ArrayList();
public Directory(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public int getSize() {
int size = 0;
Iterator it = dir.iterator();
while (it.hasNext()) {
Entry entry = (Entry) it.next();
size += entry.getSize();
}
return size;
}
@Override
public Entry add(Entry entry) {
dir.add(entry);
return this;
}
@Override
public Iterator iterator() {
return dir.iterator();
}
@Override
public void accept(Visitor v) {
v.visit(this);
}
}
- FileTreatmentException类
public class FileTreatmentException extends RuntimeException {
public FileTreatmentException() {}
public FileTreatmentException(String msg) {
super(msg);
}
}
- ListVisitor类
import java.util.Iterator;
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 it = directory.iterator();
while (it.hasNext()) {
Entry entry = (Entry)it.next();
entry.accept(this);
}
currentdir = savedir;
}
}
- FileFindVisitor类
import java.util.ArrayList;
import java.util.Iterator;
public class FileFindVisitor extends Visitor {
private String filetype;
private ArrayList found = new ArrayList();
public FileFindVisitor(String filetype) {
this.filetype = filetype;
}
public Iterator getFoundFiles() {
return found.iterator();
}
@Override
public void visit(File file) {
if (file.getName().endsWith(filetype)) {
found.add(file);
}
}
@Override
public void visit(Directory directory) {
Iterator it = directory.iterator();
while (it.hasNext()) {
Entry entry = (Entry) it.next();
entry.accept(this);
}
}
}
- Main类
import java.util.Iterator;
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 userdir = new Directory("usr");
rootdir.add(bindir);
rootdir.add(tmpdir);
rootdir.add(userdir);
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");
userdir.add(yuki);
userdir.add(hanako);
userdir.add(tomura);
yuki.add(new File("diary.html", 100));
yuki.add(new File("Composite.java", 200));
hanako.add(new File("memo.tex", 300));
hanako.add(new File("index.html", 350));
tomura.add(new File("game.doc", 400));
tomura.add(new File("junk.mail", 500));
rootdir.accept(new ListVisitor());
System.out.println("");
System.out.println("Finding HTML entries...");
FileFindVisitor ffv = new FileFindVisitor(".html");
rootdir.accept(ffv);
System.out.println("HTML files are:");
Iterator it = ffv.getFoundFiles();
while (it.hasNext()) {
File file = (File)it.next();
System.out.println(file);
}
} 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(31850)
/root/bin(30000)
/root/bin/vi(10000)
/root/bin/latex(20000)
/root/tmp(0)
/root/usr(1850)
/root/usr/yuki(300)
/root/usr/yuki/diary.html(100)
/root/usr/yuki/Composite.java(200)
/root/usr/hanako(650)
/root/usr/hanako/memo.tex(300)
/root/usr/hanako/index.html(350)
/root/usr/tomura(900)
/root/usr/tomura/game.doc(400)
/root/usr/tomura/junk.mail(500)
Finding HTML entries...
HTML files are:
diary.html(100)
index.html(350)
##Visitor模式中的角色
- Visitor(访问者)
Visitor角色负责对数据结构中每个具体的元素(ConcreteElement角色)声明一个用于访问XXXXX的visit(XXXXX)方法。visit(XXXXX)是用于处理XXXXX的方法,负责实现该方法的是ConcreteVisitor角色。在示例程序中,由Visitor类扮演此角色。
- ConcreteVisitor(具体的访问者)
ConcreteVisitor角色负责实现Visitor角色所定义的接口(API)。它要实现所有的visit(XXXXX)方法,即实现如何处理每个ConcreteElement角色。在示例程序中,由ListVisitor类和FileFindVisitor类扮演此角色。如同在ListVisitor中,currentdir字段的值在不断变化一样,随着visit(XXXXX)处理的进行,ConcreteVisitor角色的内部状态也会不断地发生变化。
- Element(元素)
Element角色表示Visitor角色的访问对象。它声明了接受访问者的accept方法。accept方法接收到的参数是Visitor角色。在示例程序中,由Element接口扮演此角色。
- ConcreteElement(具体的元素)
ConcreteElement角色负责实现Element角色所定义的接口(API)。在示例程序中,由File类和Directory类扮演此角色。
- ObjectStructure(对象结构)
ObjectStructure角色负责处理Element角色的集合。ConcreteVisitor角色为每个Element角色都准备了处理方法。在示例程序中,由Directory类扮演此角色。为了让ConcreteVisitor角色可以遍历处理每个Element角色,在示例程序中,Directory类中实现了iterator方法。
##Visitor模式的思路
- 双重分发
在Visitor模式中,调用关系如下。
element.accept(visitor);
visitor.visit(element);
可以看出,这两个方法是相反的关系。element接受visitor,而visitor又访问element。
在Visitor模式中,ConcreteElement和ConcreteVisitor这两个角色共同决定了实际进行的处理。
这种消息分发的方式一般称为双重分发(double dispatch)。
- 将处理从数据结构中分离出来
Visitor模式的目的是将处理从数据结构中分离出来。数据结构能将元素集合和元素关联在一起,但是,保存数据结构与以数据结构为基础进行处理是两种不同的操作。
ConcreteVisitor角色的开发可以独立于ConcreteElement角色,也就是说,Visitor模式提高了
ConcreteElement角色作为组件的独立性。
-
开闭原则——对扩展开放,对修改关闭
-
对扩展(extension)是开放(open)的
-
对修改(modification)是关闭(close)的
在设计类时,必须要考虑到将来可能会扩展类。绝不能毫无理由地禁止扩展类。所以“对扩展是开放的”。
但是如果在每次扩展类时都需要修改现有的类就太麻烦了。所以我们需要在不用修改现有类的前提下能够扩展类,所以“对修改是关闭的”。
我们提倡在不修改现有代码的前提下进行扩展,这就是开闭原则。
- Visitor工作的条件
在Visitor模式中,访问者只有从数据结构中获取了足够多的信息后才能工作。这样做的缺点是,如果公开了不应当被公开的信息,将来对数据结构的改良就会变得非常困难。