写点什么

这就是访问者模式

作者:千羽
  • 2023-11-08
    上海
  • 本文字数:5135 字

    阅读完需:约 17 分钟

这就是访问者模式

这就是访问者模式

一、简单介绍

访问者模式(Visitor Pattern)是一种行为设计模式,它允许在不修改现有对象结构的情况下定义新操作。该模式将操作封装在称为"访问者"的对象中,使其能够透明地访问并操作对象结构中的各个元素。


访问者模式的核心思想是将数据结构和操作分离开来。数据结构可以是一个复杂的对象组合,例如一个树形结构,而操作则是对这些对象进行的各种处理或计算。

二、应用场景

访问者模式适用于以下场景:


  1. 对象结构稳定,但其操作或功能需要频繁变化:如果你的对象结构(一组类的集合)相对稳定,但对这些对象的操作或功能需要频繁变化,访问者模式可以让你在不修改现有对象结构的情况下,定义新的操作。

  2. 需要对对象结构进行多种不相关的操作:当你需要对一个对象结构执行多种不相关的操作时,可以使用访问者模式,将这些操作封装到不同的访问者中,并在需要时调用相应的访问者进行处理。

  3. 需要对对象结构进行复杂操作:如果你的对象结构中的元素存在复杂的关联关系,并且需要进行复杂的操作时,访问者模式可以帮助你将这些操作分离出来,使代码更易于维护和扩展。

  4. 不方便或不允许修改对象结构:有些情况下,对象结构的类可能是由第三方库或框架提供的,不方便或不允许修改这些类。此时,访问者模式可以作为一种扩展方式,让你添加新的操作而不需要修改原有类。


访问模式破坏了对象的封装性,而且增加了系统的复杂度,导致修改对象结构非常困难。一般来说,你并不会用到访问者模式。当你用到它的时候,就是真正需要它的时候。

三、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); } } }}
复制代码


实际调用


//业务代码//...
//创建visitorVisitor 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 读取代码的简化版实现


//Readerpublic 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);  }}
//Visitorpublic 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

发布于: 刚刚阅读数: 9
用户头像

千羽

关注

还未添加个人签名 2019-11-28 加入

还未添加个人简介

评论

发布
暂无评论
这就是访问者模式_Java_千羽_InfoQ写作社区