设计模式:Visitor模式

通常数据结构中保存了许多元素,然后对这些元素进行处理;然而这些处理的代码实现放在哪里,通常可能就放在表示数据结构的类中,但是如果处理方式有很多种,那么每次只要增加一种处理,就必须要去修改表示数据结构的类

Visitor模式,访问者模式;这种设计模式将数据结构和处理分离开来,编写一个表示“访问者”的类来访问数据结构中的元素,并把对各元素的处理交给访问者类,这样,当需要增加新的处理时,只需要编写新的访问者,然后让数据结构可以接受访问者的访问即可

好复杂

下面是一个例子,数据结构和Composite模式例子中文件目录一样,访问者会访问由文件和文件目录构成的数据结构,然后显示出文件和文件目录一览

相关类和接口

Visitor:表示访问者的抽象类,用于访问文件和文件目录

Element:表示数据结构的接口,接受访问者的访问

ListVisitor:Visitor类的子类,显示文件和文件目录一览

Entry:File类和Directory类的超类,它是抽象类,实现了Element接口

File:表示文件的类

Directory:表示文件目录的类

FileTreatementException:表示向文件中add时发生异常的类

Main:main测试类

首先是Visitor类,访问者的抽象类,它依赖于它所访问的数据结构File类和Directory类

定义了两个抽象方法visit,但是参数不同,分别接收File类型和Directory类型的参数,从外部调用visit方法时,根据重载来选择调用

package visitor;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: Visitor
* Author: lihui
* Date: 2018/5/12 21:59
*/

public abstract class Visitor {
public abstract void visit(File file);
public abstract void visit(Directory directory);
}

Element接口,接受访问者的访问的接口;接口中声明了accept方法,参数是访问者Visitor类

package visitor;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: Element
* Author: lihui
* Date: 2018/5/12 22:00
*/

public interface Element {
public abstract void accept(Visitor v);
}

Entry类,实现了Element接口,这样Entry类适用于Visitor模式;但是Element接口中的accept抽象方法并不在Entry类中实现,而是在Entry的子类File类和Directory类中实现

add方法仅对Directory类有效,因此在Entry类中,可以进行简单地抛异常;同理,用于获取Iterator的iterator方法也仅对Directory类有效

package visitor;

import java.util.Iterator;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: Entry
* Author: lihui
* Date: 2018/5/12 22:00
*/

public abstract class Entry implements Element {
public abstract String getName();
public abstract int getSize();
public Entry add(Entry entry) throws FileTreatementException {
throw new FileTreatementException();
}

public Iterator iterator() throws FileTreatementException {
throw new FileTreatementException();
}

public String toString() {
return getName() + " (" + getSize() + ")";
}
}

File类,关键是实现了Element类里的accept抽象方法;accept方法参数是Visitor类,然后方法内部处理是调用了Visitor类的visit方法,根据重载,调用visit(File),这里this就是File累的实例

通过调用visit方法,可以告诉Visitor正在访问的对象是File类的实例this

package visitor;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: File
* Author: lihui
* Date: 2018/5/12 22:00
*/

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 0;
}

public void accept(Visitor v) {
v.visit(this);
}
}

Directory类表示文件目录

iterator方法返回Iterator,可以遍历文件目录中所有的目录条目(包括文件和文件子目录)

accept方法实现了Element接口里的抽象方法,调用了Visitor类里的visit里的visit(Directory)方法

package visitor;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: Directory
* Author: lihui
* Date: 2018/5/12 22:00
*/

public class Directory extends Entry {
private String name;
private List 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;
}

public Iterator iterator() {
return dir.iterator();
}

public void accept(Visitor v) {
v.visit(this);
}
}

ListVisitor类是Visitor类的子类,作用是访问数据结构,并显示出元素;因为ListVisitor类是Visitor的子类,所以实现了visit(File)和visit(Directory)方法

currentDir字段保存现在正在访问的文件目录名字,visit(File)方法在访问者访问文件时会被File类的accept方法调用,参数file是所访问的File类的实例;也就是说,visit(File)方法是用来实现对File类的实例要进行的处理的,在本例子中,实现的处理是先显示当前文件目录的名字currentDir,然后显示间隔符号”/“,最后显示文件名

visit(Directory)方法在访问者访问文件夹时会被Directory类的accept方法调用,参数directory是所访问的Directory类的实例;在visit(Directory)方法中实现了对Directory类的实例要进行的处理

本例中与visit(File)方法一样,先显示当前文件目录的名字,接着调用iterator方法获取文件目录的Iterator,然后通过Iterator遍历文件目录中的所有目录条目,并调用它们各自的accept方法,由于文件目录中可能存在许多的目录条目,逐一访问会非常困难

accpet方法调用visit方法,visit方法又会调用accept方法,这样递归调用十分复杂,可见Visitor模式中,accept方法和visit方法之间相互递归调用

package visitor;


import java.util.Iterator;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: ListVisitor
* Author: lihui
* Date: 2018/5/12 22:00
*/

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;
}
}

FileTreatmentException类

package visitor;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: FileTreatementException
* Author: lihui
* Date: 2018/5/12 22:00
*/

public class FileTreatementException extends RuntimeException {
public FileTreatementException() {

}

public FileTreatementException(String msg) {
super(msg);
}
}

Main,使用了访问者ListVisitor类的实例来显示Directory中的内容

package visitor;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: Main
* Author: lihui
* Date: 2018/5/12 22:00
*/

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 (FileTreatementException e) {
e.printStackTrace();
}
}
}

 

Visitor与Element之间相互调用

1:Main类生成ListVisitor实例,Directory类和File类的实例

2:Main类调用Directory类的accept方法,传递参数ListVisitor实例

3:Directory类的实例调用接收到的参数ListVisitor的visit(Directory)方法

4:ListVisitor类的实例访问文件目录,并调用找到的第一个文件的accept方法,传递this作为参数

5:File的实例调用接收到的参数ListVisitor的visit(File)方法,这时候ListVisitor的visit(Directory)还在执行中,这里并非多线程执行,而是表示visit(Directory)还存在于调用堆栈callstack中

6:从visit(File)返回到accept,接着又从accept也返回出来,然后调用另外一个File的实例的accept方法,传递ListVisitor实例this参数

7:与前面一样,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:访问者,例子中的Visitor类;负责对数据结构中每个具体的元素(ConcreteElement角色)声明一个用于访问XXXX的visit(XXXX)方法;visit(XXXX)是用于处理XXXX方法,负责实现方法的是ConcreteVisitor角色

ConcreteVisitor:具体的访问者,例子中的ListVisitor类;负责实现Visitor角色所定义的接口,实现所有的visit(XXXX)方法,也就是实现如何处理每个ConcreteVisitor角色;如同在ListVisitor中,currentDir字段的值不断发生变化一样,随着visit(XXXX)处理的进行,ConcreteVisitor角色的内部状态也会不断地发生变化

Element:Visitor角色的访问对象,例子中的Element接口;声明了接受访问者的accept方法,accept方法接收到的参数是Visitor角色

ConcreteElement:例子中的File和Directory类;负责实现Element角色定义的接口

ObjectStructure:对象结构,例子中的Directory类;负责处理Element角色的集合,ConcreteVisitor角色为每个Element角色都准备了处理方法;为了让ConcreteVisitor角色可以遍历处理每个Element角色,Directory类中实现了iterator方法

 

双重分发

accept方法的调用是:element.accept(Visitor);

visit方法的调用是:visitor.visit(element);

这两个关系相反,element接受visitor,而visitor又访问element

在Visitor模式中,ConcreteElement和ConcreteVisitor两个角色共同决定了实际进行的处理,这种消息分发方式为双重分发

 

提升复用性说明

Visitor模式的目的是将处理从数据结构中分离出来,数据结构很重要,它能将元素集合和关联在一起,但是,保存数据结构与以数据结构为基础进行处理是两种不同的东西

例子中创建了ListVisitor类作为显示文件目录内容的ConcreteVisitor角色,此外,还要编写进行其他处理的ConcreteVisitor角色,通常ConcreteVisitor角色的开发可以独立于File类和Directory类,也就是说Visitor模式提高了File和Directory类作为组件的独立性,如果将进行处理的方法定义在File和Directory类中,当每次要扩展功能,增加新的处理时,就不得不去修改File和Directory类

发表评论