十三、Visitor 模式:访问数据结构并处理数据
Visitor:访问者
我们会“处理”在数据结构中保存着的元素,通常把“处理”代码放在表示数据结构的类中。
但每增加一种处理,就不得不去修改表示数据结构的类。
在 Visitor模式中,数据结构与处理被分离。编写一个表示“访问者”的类来访问数据结构中的元素,并把对各元素的处理交给访问者类。
这样,当需要增加新的处理时,我们只需要编写新的访问者,然后让数据结构可以接受访问者的访问即可。
示例中,使用Composite模式(第11章)中用到的那个文件和文件夹的例子作为访问者要访问的数据结构。访问者会访问由文件和文件夹构成的数据结构,然后显示出文件和文件夹的一览。
示例程序类图
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 { // 生成Iterator
throw new FileTreatmentException();
}
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;
}
public String getName() {
return name;
}
public int getSize() {
return size;
}
public void accept(Visitor v) {
v.visit(this);
}
}
Directory
import java.util.Iterator;
import java.util.ArrayList;
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);
}
}
ListVisitor
import java.util.Iterator;
public class ListVisitor extends Visitor {
private String currentdir = ""; // 当前访问的文件夹的名字
public void visit(File file) { // 在访问文件时被调用
System.out.println(currentdir + "/" + file);
}
// 我们先显示当前文件夹的名字,接着调用iterator方法获取文件夹的Iterator,
// 然后通过Iterator遍历文件夹中的所有目录条目并调用它们各自的accept方法。
// 由于文件夹中可能存在着许多目录条目,逐一访问会非常困难。
// accept方法调用visit方法,visit方法又会调用accept方法,这样就形成了非常复杂的递归调用。
// 通常的递归调用是某个方法调用自身,在 Visitor模式中,则是accept方法与visit方法之间相互递归调用。
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;
}
}
FileTreatmentException
public class FileTreatmentException extends RuntimeException {
public FileTreatmentException() {
}
public FileTreatmentException(String msg) {
super(msg);
}
}
Main
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 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());
} catch (FileTreatmentException e) {
e.printStackTrace();
}
}
}
Visitor与 Element之间的相互调用
结合时序图学习示例程序的处理流程
当一个文件夹下有两个文件时,示例程序的处理流程:
① Main类生成ListVisitor的实例。示例中,Main类还生成了其他的Directory类和File类的实例,本图中省略。
② Main类调用Directory类的accept方法。这时传递的参数是ListVisitor的实例,本图中省略。
③ Directory类的实例调用接收到的参数ListVisitor的visit(Directory)方法。
④ ListVisitor类的实例会访问文件夹,并调用找到的第一个文件的accept方法。传递的参数是自身(this)。
⑤ File的实例调用接收到的参数ListVisitor的visit(File)方法。注意这时ListVisitor的visit(Directory)
还在执行中(并非多线程执行,而是表示visit(Directory)
还存在于调用堆栈(callstack)中的意思。在时序图中,表示生命周期的长方形的右侧发生了重叠就说明了这一点)。
⑥ 从visit(File)返回到accept,接着又从accept也返回出来,然后调用另外一个File的实例(同一文件夹下的第二个文件)的accept方法。传递的参数是ListVisitor的实例this。
⑦ 与前面一样,File的实例调用visit(File)方法。所有的处理完成后,逐步返回,最后回到Main类中的调用 accept方法的地方。
注意:
对于Directory类的实例和 File类的实例,调用了它们的
accept
方法
对于每一个Directory类的实例和File类的实例,其accept方法只被调用一次
对于Listvisitor 的实例,调用了它的visit(Directory)和visit(File)方法
处理visit(Directory)和visit(File)的是同一个ListVisitor的实例
在Visitor模式中,visit方法将“处理”都集中在ListVisitor里了
角色
Visitor(访问者)
负责对数据结构中每个具体的元素(ConcreteElement角色)声明一个用于访问XXXXX的
visit(XXXXX)
方法。visit(XXXXX)
是用于处理XXXXX的方法,负责实现该方法的是ConcreteVisitor角色。示例中是Visitor类。
ConcreteVisitor (具体的访问者)
负责实现Visitor角色所定义的接口(API)。它要实现所有的
visit(XXXXX)
方法,即实现如何处理每个ConcreteElement角色。示例中是ListVisitor类。
如同在ListVisitor中,currentdir字段的值不断发生变化一样,随着
visit(XXXXX)
处理的进行,ConcreteVisitor角色的内部状态也会不断地发生变化。Element(元素)
表示Visitor角色的访问对象。它声明了接受访问者的accept方法。
accept方法接收到的参数是 Visitor角色。
示例中是Element 接口。
ConcreteElement
负责实现Element角色所定义的接口(API)。
示例中是File类和Directory类。
ObjectStructure (对象结构)
负责处理Element角色的集合。
ConcreteVisitor角色为每个Element角色都准备了处理方法。
示例中是Directory类(一人分饰两角)。
为了让ConcreteVisitor角色可以遍历处理每个Element角色,示例中在Directory类中实现了iterator方法。
扩展思路的要点
双重分发
Visitor模式中方法的调用关系。
accept(接受)方法的调用方式:element.accept(visitor);
visit(访问)方法的调用方式:visitor.visit(element);
它们是相反的关系。element接受visitor,而visitor又访问element
ConcreteElement 和 ConcreteVisitor这两个角色共同决定了实际进行的处理。
这种消息分发的方式一般被称为双重分发(double dispatch)。
为什么要弄得这么复杂
Visitor模式的目的是将处理从数据结构中分离出来。数据结构很重要,它能将元素集合和关联在一起。
但是注意,保存数据结构与以数据结构为基础进行处理是两种不同的东西。
示例中,创建了ListVisitor类作为显示文件夹内容的ConcreteVisitor角色。
还可以编写进行其他处理的ConcreteVisitor角色,该角色可以独立于File类和 Directory类,即,Visitor模式提高了File类和Directory类作为组件的独立性。
如果将进行处理的方法定义在File类和Directory类中,当每次要扩展功能,增加新的“处理”时,就只能修改File类和Directory类。
开闭原则——对扩展开放,对修改关闭
关于功能扩展和修改,有个开闭原则(The Open-Closed Principle, OCP)。
对扩展(extension)是开放(open)的
对修改(modification)是关闭(close)的
易增加 ConcreteVisitor角色
使用Visitor模式可以很容易地增加ConcreteVisitor角色。因为具体的处理被交给ConcreteVisitor角色负责,因此完全不用修改ConcreteElement角色。
难增加 ConcreteElement角色
例如,假设现在要在示例中增加Entry类的子类Device类。即,Device类是File类和Directory类的兄弟类。
这时,我们需要在Visitor类中声明一个visit(Device)方法,并在所有的Visitor类的子类中都实现这个方法。
Visitor 工作所需的条件
“在 Visitor模式中,对数据结构中的元素进行处理的任务被分离出来,交给Visitor类负责。
这样,就实现了数据结构与处理的分离”这个主题。
但是有个前提条件:Element角色必须向Visitor角色公开足够多的信息。
例如示例中,visit(Directory)方法需要调用每个目录条目的accept方法。为此,Directory类必须提供用于获取每个目录条目的iterator方法。
访问者只有从数据结构中获取了足够多的信息后才能工作。这样缺点是,若公开了不应当被公开的信息,将来很难改良数据结构。
相关的设计模式
Iterator模式(第1章)
Iterator模式和 Visitor模式都是在某种数据结构上进行处理。
Iterator 模式用于逐个遍历保存在数据结构中的元素。
Visitor 模式用于对保存在数据结构中的元素进行某种特定的处理。Composite 模式(第11章)
有时访问者所访问的数据结构会使用Composite模式。Interpreter模式(第23章)
在Interpreter模式中,有时会使用 Visitor模式。例如,在生成了语法树后,可能会使用 Visitor
模式访问语法树的各个节点进行处理。
十四、Chain of Responsibility模式:推卸责任
Chain of Responsibility:职责链
例如,外部请求程序进行某个处理,但程序暂时无法直接决定由哪个对象负责处理时,就需要推卸责任。
这种情况下,我们可以考虑将多个对象组成一条职责链,然后按照它们在职责链上的顺序一个一个地找出到底应该谁来负责处理。
当一个人被要求做什么事情时,如果他可以做就自己做,如果不能做就将“要求”转给另外一个人。下一个人如果可以自己处理,就自己做;如果也不能自己处理,就再转给另外一个人…这就是Chain of Responsibility模式。
示例程序类图
注意 Support抽象类 的 support方法
如果resolve方法返回false,则support方法会将问题转交给下一个对象。
如果已经到达职责链中的最后一个对象,则表示没有人处理问题,将会显示出处理失败的相关信息。
在本例中我们只是简单地输出处理失败的相关信息,但根据需求不同,有时候也需要抛出异常。
Trouble
public class Trouble {
private int number; // 问题编号
public Trouble(int number) { // 生成问题
this.number = number;
}
public int getNumber() { // 获取问题编号
return number;
}
public String toString() { // 代表问题的字符串
return "[Trouble " + number + "]";
}
}
Support
public abstract class Support {
private String name; // 解决问题的实例的名字
private Support next; // 要推卸给的对象
public Support(String name) { // 生成解决问题的实例
this.name = name;
}
public Support setNext(Support next) { // 设置要推卸给的对象
this.next = next;
return next;
}
public void support(Trouble trouble) { // 解决问题的步骤
if (resolve(trouble)) {
done(trouble);
} else if (next != null) {
next.support(trouble);
} else {
fail(trouble);
}
}
public String toString() { // 显示字符串
return "[" + name + "]";
}
protected abstract boolean resolve(Trouble trouble); // 解决问题的方法
protected void done(Trouble trouble) { // 解决
System.out.println(trouble + " is resolved by " + this + ".");
}
protected void fail(Trouble trouble) { // 未解决
System.out.println(trouble + " cannot be resolved.");
}
}
NoSupport
public class NoSupport extends Support {
public NoSupport(String name) {
super(name);
}
protected boolean resolve(Trouble trouble) { // 解决问题的方法
return false; // 自己什么也不处理
}
}
LimitSupport
public class LimitSupport extends Support {
private int limit; // 可以解决编号小于limit的问题
public LimitSupport(String name, int limit) { // 构造函数
super(name);
this.limit = limit;
}
protected boolean resolve(Trouble trouble) { // 解决问题的方法
if (trouble.getNumber() < limit) {
return true;
} else {
return false;
}
}
}
OddSupport
public class OddSupport extends Support {
public OddSupport(String name) { // 构造函数
super(name);
}
protected boolean resolve(Trouble trouble) { // 解决问题的方法
if (trouble.getNumber() % 2 == 1) {
return true;
} else {
return false;
}
}
}
SpecialSupport
public class SpecialSupport extends Support {
private int number; // 只能解决指定编号的问题
public SpecialSupport(String name, int number) { // 构造函数
super(name);
this.number = number;
}
protected boolean resolve(Trouble trouble) { // 解决问题的方法
if (trouble.getNumber() == number) {
return true;
} else {
return false;
}
}
}
Main
public class Main {
public static void main(String[] args) {
Support alice = new NoSupport("Alice");
Support bob = new LimitSupport("Bob", 100);
Support charlie = new SpecialSupport("Charlie", 429);
Support diana = new LimitSupport("Diana", 200);
Support elmo = new OddSupport("Elmo");
Support fred = new LimitSupport("Fred", 300);
// 形成职责链
alice.setNext(bob).setNext(charlie).setNext(diana).setNext(elmo).setNext(fred);
// 制造各种问题
for (int i = 0; i < 500; i += 33) {
alice.support(new Trouble(i));
}
}
}
角色
Handler(处理者)
定义处理请求的接口(API)。
它知道“下一个处理者”是谁,如果自己无法处理请求,它会将请求转给“下一个处理者”(也是Handler角色)。
示例中是Support类,负责处理请求的是support方法。ConcreteHandler(具体的处理者)
处理请求的具体角色。示例中是NoSupport、LimitSupport、OddSupport、SpecialSupport等各个类扮演此角色。Client(请求者)
向第一个ConcreteHandler角色发送请求的角色。示例中是Main类。
拓展思路的要点
弱化了发出请求的人(Client角色)和处理请求的人(ConcreteHandler角色)之间的关系
这是本模式最大优点。
如果不使用该模式,就必须有某个伟大的角色知道“谁应该处理什么请求”,这有点类似中央集权制。
而让“发出请求的人”知道“谁应该处理该请求”并不明智,会降低其作为可复用的组件的独立性。
补充说明:简单起见,示例中让扮演Client角色的Main类负责串联起ConcreteHandler的职责链。
可动态地改变职责链
示例中解决问题是按固定顺序进行处理的,但要考虑负责处理的各个ConcreteHandler角色之间的关系可能会发生变化的情况。
若使用本模式,通过委托推卸责任,可根据情况变化动态地重组职责链;
否则,在程序中固定写明“某个请求需要谁处理”这样的对应关系,很难在程序运行中去改变请求的处理者。
在视窗系统中,用户有时需要可以自由地在视窗中添加控件(按钮和文本输入框等),就很适合用本模式。
专注于自己的工作
每个ConcreteHandler角色都专注于自己所负责的处理。当自己无法处理时,会将请求转出去。
若不使用本模式,则需要编写一个“决定谁应该负责什么样的处理”的方法,亦或是让每个ConcreteHandler角色自己负责“任务分配”工作,
即“如果自己不能处理,就转交给那个人。如果他也不能处理,那就根据系统情况将请求再转交给另外一个人”。
推卸请求会导致处理延迟
与“事先确定哪个对象负责什么样的处理,当接收到请求时,立即让相应的对象去处理请求”相比,使用本模式确实导致处理请求发生了延迟。
需要权衡一下。若请求和处理者之间的关系是确定的,且需非常快的处理速度时,不使用本模式更好。
相关的设计模式
Composite 模式(第11章)
Handler 角色经常会使用Composite模式。Command模式(第23章)
有时会使用Command模式向 Handler 角色发送请求。