定义
访问者模式(Visitor Pattern)的英文定义如下:
Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
意思是:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。简单地来说,就是将数据结构和数据操作相分离。
比如说,做过开发的都知道,普通用户和 Root 用户在 Linux 机器上对文件或者文件夹进行操作时,Root 用户往往不受限制,而普通用户却需要各种访问权限才能正常进行,这种同样是访问文件或文件夹,不同的访问者表现出的行为不同的现象就是我们的访问者模式,文件或文件夹就是被访问的元素,Root 用户或者普通用户就是我们的访问者,而文件或文件夹往往是位于计算机或者其它存储设备上的,这里的存储设备就是访问者模式中的 ObjectStructure,可以类比为元素的容器对象。
组成角色
访问者模式包含角色如下:
- 访问者(Visitor):定义对不同的元素进行访问时的抽象行为,一般来说,有多少个具体元素,就有多少个抽象接口;
- 具体访问者(ConcreteVisitor):实现上面 Visitor 定义的所有接口,用来指定该访问者对各个元素进行访问时的具体行为,在本文中由 Root 用户和普通用户扮演该角色;
- 元素(Element):抽象的被访问的元素,一般会定义一个 accept 方法,指定其被访问时的抽象行为;
- 具体元素(ConcreteElement):具体的被访问的元素,实现上面 Element 的 accept 方法,各个元素负责定义自己的 accept 行为,来表示其被访问时的行为,本文中由 FileElement 和 DictionaryElement 类扮演;
- 对象结构(ObjectStructure):对象结构实际上是一个被访问元素的集合,好比一个元素容器,对容器的具体元素的访问表现出的行为如何,这是由访问者模式决定的。
角色之间的 UML 关系图如下:
访问者模式代码实现
抽象访问者
// 抽象的访问者角色,需要针对每个被访问元素都定义一个接口
public interface Visitor {
void visit(ConcreteElementA element);
}
具体访问者
// 具体的访问者
public class ConcreteVisitorA implements Visitor {
@Override
public void visit(ConcreteElementA element){
// 实现自己的对元素ConcreteElementA的访问行为
}
}
抽象元素
// 被访问元素的抽象
public interface Element {
// 声明 accept 方法,代表元素可以被访问
void accept(Visitor visitor);
}
具体元素
// 实现抽象元素接口
public class ConcreteElementA implements Element {
@Override
public void accept(Visitor visitor) {
// 传入对象自身
visitor.visit(this);
}
}
对象结构
public class ObjectStructure {
private List<Element> elements;
public ObjectStructure(List<Element> elements) {
this.elements = elements;
}
// 自定义方法,该方法往往要对 elements 进行遍历
public void show(Visitor visitor) {
for(Element element: elements) {
element.accept(visitor);
}
}
}
优缺点
访问者模式的优点:
- 数据结构和数据操作相分离;
- 对访问者拓展性良好,只需要增加新的访问者类即可;
- 各个角色职责明确,符合单一职责原则。
访问者模式的缺点:
- 元素变更时会导致整个代码都要调整。
应用场景
访问者模式的典型应用场景如下:
- 对象的结构(元素)比较稳定,而访问者频繁变动的场景;
- 数据操作和数据结构分离的场景。
使用实例
还是以文章开始讲的 Root 用户和普通用户访问文件或文件夹为例,下面我们以访问者模式来实现一下:
抽象的访问元素
// 被访问的元素
interface Element {
void accept(Visitor visitor);
}
具体访问元素 —— 文件元素
// 文件元素
class FileElement implements Element {
// 当前元素的访问权限
private String lookPerms;
// 文件名
private String name;
FileElement(String name, String lookPerms) {
this.name = name;
this.lookPerms = lookPerms;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
String getName() {
return name;
}
String getLookPerms() {
return lookPerms;
}
}
具体访问元素 —— 文件夹元素
// 文件夹
class DictionaryElement implements Element {
// 当前元素的访问权限
private String lookPerms;
// 文件夹名
private String name;
DictionaryElement(String name, String lookPerms) {
this.name = name;
this.lookPerms = lookPerms;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
String getName() {
return name;
}
String getLookPerms() {
return lookPerms;
}
}
抽象的访问者
// 访问者
interface Visitor {
// 定义对不同的元素(文件)进行访问时的具体行为
void visit(FileElement fileElement);
// 定义对不同的元素(文件夹)进行访问时的具体行为
void visit(DictionaryElement dictionaryElement);
}
具体访问者 ——Root 用户
// root用户
class RootVisitor implements Visitor {
@Override
public void visit(FileElement fileElement) {
System.out.println("当前:Root " + "要访问的文件名:" + fileElement.getName() + " 允许访问!");
}
@Override
public void visit(DictionaryElement dictionaryElement) {
System.out.println("当前:Root " + "要访问的文件夹名:" + dictionaryElement.getName() + " 允许访问!");
}
}
具体访问者 —— 普通用户
// 普通用户
class NormalVisitor implements Visitor {
// 定义该用户具备的权限集合
private List<String> lookPerms;
NormalVisitor(List<String> lookPerms) {
this.lookPerms = lookPerms;
}
@Override
public void visit(FileElement fileElement) {
if (this.lookPerms.indexOf(fileElement.getLookPerms()) >= 0) {
System.out.println("当前:普通用户" + " 具备权限:" + this.lookPerms.toString() + " 要访问的文件名:" + fileElement.getName() + " 允许访问!");
} else {
System.out.println("当前:普通用户" + " 具备权限:" + this.lookPerms.toString() +" 要访问的文件名:" + fileElement.getName() + " 权限不足,禁止访问!");
}
}
@Override
public void visit(DictionaryElement dictionaryElement) {
if (this.lookPerms.indexOf(dictionaryElement.getLookPerms()) >= 0) {
System.out.println("当前:普通用户" + " 具备权限:" + this.lookPerms.toString() + " 要访问的文件夹名:" + dictionaryElement.getName() + " 允许访问!");
} else {
System.out.println("当前:普通用户" + " 具备权限:" + this.lookPerms.toString() +" 要访问的文件夹名:" + dictionaryElement.getName() + " 权限不足,禁止访问!");
}
}
}
存储设备
// ObjectStructure角色
class Computor {
// 计算机中的文件和文件夹List
private List<Element> elementList = new LinkedList<>();
{
elementList.add(new FileElement("Java讲义.pdf", "look-file"));
elementList.add(new DictionaryElement("program", "look-dictionary"));
}
// 展示该电脑中的文件和文件夹
void showFileAndDict(Visitor visitor) {
for (Element element: elementList) {
element.accept(visitor);
}
}
}
客户端
public static void main(String[] args) {
Computor computor = new Computor();
computor.showFileAndDict(new NormalVisitor(Collections.singletonList("look-dictionary")));
computor.showFileAndDict(new RootVisitor());
computor.showFileAndDict(new NormalVisitor(Arrays.asList("look-file", "look-dictionary")));
}
输出结果如下:
当前:普通用户 具备权限:[look-dictionary] 要访问的文件名:Java 讲义.pdf 权限不足,禁止访问!
当前:普通用户 具备权限:[look-dictionary] 要访问的文件夹名:program 允许访问!
当前:Root 要访问的文件名:Java 讲义.pdf 允许访问!
当前:Root 要访问的文件夹名:program 允许访问!
当前:普通用户 具备权限:[look-file, look-dictionary] 要访问的文件名:Java 讲义.pdf 允许访问!
当前:普通用户 具备权限:[look-file, look-dictionary] 要访问的文件夹名:program 允许访问!
总结
访问者模式适应于元素种类基本不变但是 visit 行为变化的场景,或者说访问者不断增加的场景,访问者增加时我们只需要增加新的访问者类即可,一定程度上避免了在 visit 上进行 if…else 的繁杂逻辑判断,减少了代码体积。