这就是访问者模式
一、简单介绍
访问者模式(Visitor Pattern)是一种行为设计模式,它允许在不修改现有对象结构的情况下定义新操作。该模式将操作封装在称为"访问者"的对象中,使其能够透明地访问并操作对象结构中的各个元素。
访问者模式的核心思想是将数据结构和操作分离开来。数据结构可以是一个复杂的对象组合,例如一个树形结构,而操作则是对这些对象进行的各种处理或计算。
二、应用场景
访问者模式适用于以下场景:
对象结构稳定,但其操作或功能需要频繁变化:如果你的对象结构(一组类的集合)相对稳定,但对这些对象的操作或功能需要频繁变化,访问者模式可以让你在不修改现有对象结构的情况下,定义新的操作。
需要对对象结构进行多种不相关的操作:当你需要对一个对象结构执行多种不相关的操作时,可以使用访问者模式,将这些操作封装到不同的访问者中,并在需要时调用相应的访问者进行处理。
需要对对象结构进行复杂操作:如果你的对象结构中的元素存在复杂的关联关系,并且需要进行复杂的操作时,访问者模式可以帮助你将这些操作分离出来,使代码更易于维护和扩展。
不方便或不允许修改对象结构:有些情况下,对象结构的类可能是由第三方库或框架提供的,不方便或不允许修改这些类。此时,访问者模式可以作为一种扩展方式,让你添加新的操作而不需要修改原有类。
访问模式破坏了对象的封装性,而且增加了系统的复杂度,导致修改对象结构非常困难。一般来说,你并不会用到访问者模式。当你用到它的时候,就是真正需要它的时候。
三、UML 图
初次认识访问者模式的话,这张图看不出来什么信息,反而会更迷惑,直接看以下 demo 吧。
四、示例
下面是一个使用 Java 实现的访问者模式示例:
首先,我们定义一个元素接口(Element),它声明了一个接受访问者对象的方法(accept):
//假定这是一组复杂的数据结构
public interface Element {
void accept(Visitor visitor);
}
复制代码
然后,我们实现几个具体的元素类(ConcreteElementA 和 ConcreteElementB),它们都实现了元素接口:
public class ConcreteElementA implements Element {
public void accept(Visitor visitor) {
visitor.visitConcreteElementA(this);
}
public void operationA() {
// 元素A的特定操作
}
}
public class ConcreteElementB implements Element {
public void accept(Visitor visitor) {
visitor.visitConcreteElementB(this);
}
public void operationB() {
// 元素B的特定操作
}
}
复制代码
接下来,我们定义一个访问者接口(Visitor),它声明了对每个具体元素进行处理的方法:
public interface Visitor {
void visitConcreteElementA(ConcreteElementA elementA);
void visitConcreteElementB(ConcreteElementB elementB);
}
复制代码
然后,我们实现具体的访问者类(ConcreteVisitor),它实现了访问者接口,并对每个具体元素进行具体的处理:
public class ConcreteVisitor implements Visitor {
public void visitConcreteElementA(ConcreteElementA elementA) {
// 处理元素A的操作
elementA.operationA();
}
public void visitConcreteElementB(ConcreteElementB elementB) {
// 处理元素B的操作
elementB.operationB();
}
}
复制代码
最后,我们定义一个对象结构类(ObjectStructure),它包含了一组元素,并提供了对这些元素进行访问的方法(注意:这个对象结构类的参数结构不应该经常变化):
import java.util.ArrayList;
import java.util.List;
public class ObjectStructure {
private List<Element> elements = new ArrayList<>();
public void addElement(Element element) {
elements.add(element);
}
public void removeElement(Element element) {
elements.remove(element);
}
public void accept(Visitor visitor) {
for (Element element : elements) {
element.accept(visitor);
}
}
}
复制代码
现在,我们可以使用访问者模式了。下面是一个示例的使用代码:
public class Main {
public static void main(String[] args) {
ObjectStructure objectStructure = new ObjectStructure();
objectStructure.addElement(new ConcreteElementA());
objectStructure.addElement(new ConcreteElementB());
Visitor visitor = new ConcreteVisitor();
objectStructure.accept(visitor);
}
}
复制代码
五、实际使用
6.1.数据结构
有一个树状结构,分为 Node(节点)和 Flow(连线),对应到图上的话,节点下方有连线,连线下方有节点。如此构成树状结构。节点和连线均有多种类型。
简化后的类图和代码如下:
public class Node {
//其他字段
//...
//节点对应的连线
public List<Flow> flows;
}
public class Flow {
//连线上一个节点
public Node preNode;
//连线下一个节点
public Node nextNode;
}
复制代码
6.2.需求描述
1.需要在做一些操作之前,将树状结构中的 HttpNode 和 SequenceFlow 的一些字段赋值
2.在一些操作之后,清空上面赋值的这些数据
3.收到一些数据后,更新所有 Node 节点的数据
6.3 需求实现
根据以上需求,需要对这个复杂的数据结构做多种操作,并且多种操作的节点并不相同。
如果不使用设计模式的话,每次操作都需要重新写一次遍历这棵树的代码,并过滤出指定节点进行指定操作
使用访问者模式可以封装遍历的逻辑,简化节点的访问方式,增强代码的扩展性和可维护性
以下是具体代码实现(根据实际使用对模板访问者结构做了一些改变):
修改数据结构
直接在父类中定义 accept 方法,应对不针对特定节点的通用处理
public class Node {
//其他字段
//...
//节点对应的连线
public List<Flow> flows;
//定义接受访问者对象的方法
public void accept(Visitor visitor) {
visitor.visitNode(this);
}
}
复制代码
public class HttpNode {
//其他字段
//...
//定义接受访问者对象的方法
@Override
public void accept(Visitor visitor) {
super.accept(visitor);
//对于需要识别指定节点的方法,允许访问者直接访问
visitor.visitHttpNode(this);
}
}
复制代码
定义访问者
public interface Visitor {
void visitNode(Node node);
void visitHttpNode(HttpNode node);
void visitStartNode(Start node);
void visitEndNode(End node);
}
复制代码
访问者的实现
由于多数节点不需要特殊处理,所以使用 AbstractVisitor 实现 Visitor,并提供 Visitor 接口的空实现。具体 Visitor 需要哪些方法自己 Override 即可
//为每个节点设置mock数据
public class MockDataSetVisitor extends AbstractVisitor {
//key: nodeId, value: mockData
private final Map<String, MockDataInfo> mockDataMap;
public MockDataSetVisitor(Map<String, MockDataInfo> mockDataMap) {
this.mockDataMap = mockDataMap;
}
@Override
public void visitNode(Node node) {
MockDataInfo mockDataInfo = mockDataMap.get(node.getId());
if (mockDataInfo != null) {
node.setMockId(mockDataInfo.getMockId());
node.setMockData(mockDataInfo.getMockData());
}
}
}
//清除每个节点的mock数据
public class MockDataClearVisitor extends AbstractVisitor {
@Override
public void visitNode(Node node) {
node.setMockData(null);
node.setMockId(null);
}
}
复制代码
对象结构访问
封装遍历对象的方法
public class NodeStructure {
/**
* 从startNode开始遍历流程图
*/
public void accept(Visitor visitor, Node startNode) {
if (startNode == null) {
return;
}
List<String> executedNodeIds = new ArrayList<>();
traversalNode(visitor, startNode, executedNodeIds);
}
/**
* 遍历节点
* @param executedNodeIds 遍历过的nodeId列表 防止无限递归
*/
private void traversalNode(Visitor visitor, Node node, List<String> executedNodeIds) {
if (executedNodeIds.contains(node.getId())) {
return;
}
node.accept(visitor);
executedNodeIds.add(node.getId());
List<Flow> sequenceFlows = node.getSequenceFlows();
if (CollUtil.isEmpty(sequenceFlows)) {
return;
}
for (Flow sequenceFlow : sequenceFlows) {
Node nextNode = sequenceFlow.getNextNode();
if (nextNode != null) {
traversalNode(visitor, nextNode, executedNodeIds);
}
}
}
}
复制代码
实际调用
//业务代码
//...
//创建visitor
Visitor visitor = new MockDataSetVisitor(mockDataMap);
//使用visitor访问节点
nodeStructure.accept(visitor, startNode);
复制代码
六、典型用例
5.1.ASM
org.objectweb.asm 是一个用于 Java 字节码操作的开源库,它允许 Java 开发人员直接操作、生成和转换 Java 字节码。具体用法 这篇文章 写的不错,可做参考,这里主要分析 Asm 在解析 Class 使用到的访问者模式。
Asm 解析 Class 主要用到两个类,ClassReader 和 ClassVisitor。
先看一下 Asm 读取代码的简化版实现
//Reader
public class ClassReader {
//构造函数,通过class字节码文件构造
public ClassReader(final byte[] classFile) {
this(classFile, 0, classFile.length);
}
//根据传入的visitor,访问解析出来的类数据
public void accept(final ClassVisitor classVisitor, final int parsingOptions) {
accept(classVisitor, new Attribute[0], parsingOptions);
}
}
//Visitor
public abstract class ClassVisitor {
public void visit(
final int version,
final int access,
final String name,
final String signature,
final String superName,
final String[] interfaces) {
if (api < Opcodes.ASM8 && (access & Opcodes.ACC_RECORD) != 0) {
throw new UnsupportedOperationException("Records requires ASM8");
}
if (cv != null) {
cv.visit(version, access, name, signature, superName, interfaces);
}
}
public void visitSource(final String source, final String debug) {
if (cv != null) {
cv.visitSource(source, debug);
}
}
}
复制代码
ClassReader 相当于 UML 图中的 ObjectStructure 类,主要提供数据的解析和遍历
ClassVisitor 相当于 UML 图中的 Visitor 类,通过重写其方法,来访问并操作解析出来的数据
为什么没有最重要的 Element 类?
这里Element
实际上就是 ClassReader 解析出来的类信息,包括类名/描述符/方法/字段等,这些信息被存在 ClassReader 中,并通过其 accept 方法进行遍历
基于此,想要访问解析出的类信息只需要自定义 Visitor 并传给 ClassReader 即可,代码如下:
class CustomVisitor extends ClassVisitor{
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super(version, access, name, signature, superName, interfaces);
System.out.println("类名: " + name);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
System.out.println("字段: " + name + " 描述符: " + descriptor);
return new FieldParser();
}
}
public static void main(String[] args) {
ClassReader reader = new ClassReader("com/github/nickid2018/asm/TestClass");
CustomVisitor cv = new CustomVisitor();
reader.accept(cv, 0);
}
复制代码
注:这里只是抽取出 Asm 使用 Visitor 设计模式的用法思想,实际实现要复杂很多。详见 ASM 源码。
七、总结
访问者模式看起来复杂,实际只是针对复杂数据结构(主要是树形结构和复杂集合)的内部访问做了一层封装,简化了操作对象内部结构的方法。所以这种设计模式比较针对特定的数据结构,大多数时候并不适用,但是正如上面所说,当你用到它的时候,就是真正需要它的时候。
Photo by Luca Bravo on Unsplash
评论