文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

深入浅出设计模式【二十三、访问者模式】

访问者模式详解

一、访问者模式介绍

访问者模式是一种行为型设计模式,它允许你将算法与对象结构分离。该模式的核心思想是:定义不改变对象结构的情况下操作结构中元素的新操作

访问者模式解决了以下关键问题:

  • 当需要在不修改现有类的前提下向类层次结构添加新功能
  • 当对象结构包含许多不同类型的对象,需要对这些对象执行依赖于其具体类型的操作
  • 需要在多个不同类上执行相同操作,但每个类的操作实现不同

在软件设计中,访问者模式是典型的**“双重分派”**模式,它提供了一种将算法与对象结构分离的方法,符合开闭原则(对扩展开放,对修改关闭)。

二、核心概念与意图

核心概念

  1. 访问者(Visitor)

    • 声明对对象结构中元素操作的访问方法
    • 为每种具体元素类定义 visit 方法
    • 可以累积状态(实现状态访问者)
  2. 具体访问者(ConcreteVisitor)

    • 实现访问者声明的每个操作
    • 每个操作实现对象结构中一个类对应算法的片段
  3. 元素(Element)

    • 声明 accept 方法,接收一个访问者参数
    • 方法签名:accept(Visitor v)
  4. 具体元素(ConcreteElement)

    • 实现 accept 方法,典型实现:v.visit(this)
    • 通过将自身作为参数传递给访问者,实现双重分派
  5. 对象结构(Object Structure)

    • 能枚举其元素
    • 可以是一个集合、复合结构或列表
    • 提供高层接口允许访问者访问其元素

意图

  • 定义作用于对象结构中各元素的操作,但不改变元素类
  • 封装分布在多个类中的相关操作
  • 在对象结构稳定但操作可能频繁变化时保持灵活性

三、适用场景剖析

访问者模式适用于:

  1. 处理复杂对象结构

    • 当对象结构包含许多具有不同接口的类,需要对所有对象执行操作
    • 例如:编译器中的抽象语法树(AST)处理
  2. 跨类结构的横切关注点

    • 需要在不修改类的前提下添加相关操作
    • 例如:统计分析、日志记录、持久化操作
  3. 行为分离

    • 当对象结构相对稳定但操作频繁变化
    • 例如:文档导出功能(PDF、HTML、纯文本等格式)
  4. 累积状态操作

    • 当算法需要跨多个对象累积状态时
    • 例如:计算对象集合的总价格或总数

不适用场景

  • 对象结构频繁变化(添加新元素会破坏所有访问者)
  • 元素接口不稳定(改变元素接口会破坏所有访问者)
  • 简单对象结构(使用访问者模式会过度复杂化)

四、UML类图解析(Mermaid)

dependency
dependency
composition
dependency
«interface»
Visitor
+visitConcreteElementA(element: ConcreteElementA)
+visitConcreteElementB(element: ConcreteElementB)
ConcreteVisitor1
+visitConcreteElementA(element: ConcreteElementA)
+visitConcreteElementB(element: ConcreteElementB)
ConcreteVisitor2
+visitConcreteElementA(element: ConcreteElementA)
+visitConcreteElementB(element: ConcreteElementB)
«interface»
Element
+accept(visitor: Visitor)
ConcreteElementA
+featureA()
+accept(visitor: Visitor)
ConcreteElementB
+featureB()
+accept(visitor: Visitor)
ObjectStructure
-elements: List<Element>
+addElement(element: Element)
+accept(visitor: Visitor)

流程说明

  1. 客户端创建具体访问者对象
  2. 客户端创建对象结构并填充具体元素
  3. 客户端调用对象结构的 accept 方法,传入访问者
  4. 对象结构遍历其元素,对每个元素调用 accept(visitor)
  5. 元素调用访问者的 visit(this) 方法(双重分派)
  6. 访问者对元素执行具体操作

五、各种实现方式及其优缺点

1. 标准实现(接口+类层次)

最常见实现方式,符合访问者模式的原始定义:

// 元素接口
public interface Element {
    void accept(Visitor visitor);
}

// 具体元素A
public class ConcreteElementA implements Element {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    
    public String operationA() {
        return "Element A operation";
    }
}

// 具体元素B
public class ConcreteElementB implements Element {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    
    public String operationB() {
        return "Element B operation";
    }
}

// 访问者接口
public interface Visitor {
    void visit(ConcreteElementA element);
    void visit(ConcreteElementB element);
}

// 具体访问者1
public class ConcreteVisitor1 implements Visitor {
    @Override
    public void visit(ConcreteElementA element) {
        System.out.println("Visitor1: " + element.operationA());
    }
    
    @Override
    public void visit(ConcreteElementB element) {
        System.out.println("Visitor1: " + element.operationB());
    }
}

// 具体访问者2
public class ConcreteVisitor2 implements Visitor {
    @Override
    public void visit(ConcreteElementA element) {
        System.out.println("Visitor2 processing: " + element.operationA());
    }
    
    @Override
    public void visit(ConcreteElementB element) {
        System.out.println("Visitor2 processing: " + element.operationB());
    }
}

// 对象结构
public class ObjectStructure {
    private List<Element> elements = new ArrayList<>();
    
    public void addElement(Element element) {
        elements.add(element);
    }
    
    public void accept(Visitor visitor) {
        elements.forEach(e -> e.accept(visitor));
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        ObjectStructure structure = new ObjectStructure();
        structure.addElement(new ConcreteElementA());
        structure.addElement(new ConcreteElementB());
        
        Visitor visitor1 = new ConcreteVisitor1();
        structure.accept(visitor1);
        
        Visitor visitor2 = new ConcreteVisitor2();
        structure.accept(visitor2);
    }
}

优点

  • 清晰分离数据结构与操作
  • 添加新操作只需创建新访问者类
  • 操作相关代码集中在一处
  • 符合开闭原则(操作扩展不影响元素类)

缺点

  • 添加新元素需要修改所有访问者接口
  • 违反依赖倒置原则(访问者依赖具体元素类)
  • 元素类需暴露足够信息供访问者操作

2. 反射访问者实现(Java反射)

利用反射机制绕过接口方法声明限制:

public class ReflectiveVisitor implements Visitor {
    public void visit(Element element) {
        try {
            Method visitMethod = getClass().getMethod("visit", element.getClass());
            visitMethod.invoke(this, element);
        } catch (Exception e) {
            // 处理未实现的情况
        }
    }
    
    public void visit(ConcreteElementA element) {
        System.out.println("Visiting ElementA: " + element.operationA());
    }
    
    public void visit(ConcreteElementB element) {
        System.out.println("Visiting ElementB: " + element.operationB());
    }
}

优点

  • 添加新元素只需实现新方法,不修改接口
  • 避免元素接口膨胀

缺点

  • 类型检查在运行时而非编译时
  • 反射性能开销
  • 方法签名不明确降低代码可读性
  • 错误处理复杂

3. Lambda实现(Java 8+)

对于简单操作,可使用函数式接口简化:

public class LambdaVisitor implements ElementVisitor {
    private Map<Class<? extends Element>, Consumer<? extends Element>> handlers = new HashMap<>();
    
    public <T extends Element> void registerHandler(Class<T> type, Consumer<T> handler) {
        handlers.put(type, handler);
    }
    
    @Override
    public void visit(Element element) {
        Consumer<Element> handler = (Consumer<Element>) handlers.get(element.getClass());
        if (handler != null) {
            handler.accept(element);
        }
    }
}

// 使用
LambdaVisitor visitor = new LambdaVisitor();
visitor.registerHandler(ConcreteElementA.class, 
    element -> System.out.println("Lambda visiting A: " + element.operationA()));

优点

  • 简洁灵活
  • 避免创建大量小类
  • 适合一次性操作

缺点

  • 不适用于复杂状态访问者
  • 类型转换不安全
  • 操作逻辑分散

六、最佳实践

  1. 识别稳定部分

    • 访问者模式最适合对象结构稳定的场景
    • 元素类变化频率应远低于访问者
  2. 避免访问者状态污染

    • 访问者应为无状态或方法局部状态
    • 全局状态使用独立数据结构管理
  3. 访问者接口版本化

    • 使用版本命名:VisitorV1VisitorV2
    • 旧版访问者处理遗留元素(Deprecated元素)
  4. 组合访问者

    • 创建组合访问者统一处理多种操作
    • 减少对对象结构的遍历次数
  5. 空访问者实现

    • 提供默认访问者避免实现所有方法
    public abstract class DefaultVisitor implements Visitor {
        @Override
        public void visit(ConcreteElementA element) {}
        
        @Override
        public void visit(ConcreteElementB element) {}
    }
    
  6. 文档化访问者契约

    • 清晰说明访问操作的前提和后置条件
    • 记录操作之间的依赖关系

七、在开发中的演变和应用

1. 语言扩展应用

  • 编译器设计:抽象语法树(AST)处理
  • 注解处理器:Java注解处理API(APT)
  • 语法分析器:ANTLR等解析器生成器

2. 数据转换框架

  • 对象序列化:Jackson/Gson等JSON库中的序列化器
  • 协议转换:不同API/协议间转换(REST↔gRPC)
  • ETL工具:数据提取转换过程

3. 基础设施自动化

  • 基础设施即代码:Terraform资源访问
  • 配置管理:统一配置校验、部署处理
  • 安全扫描:对系统各组件执行安全检查

4. UI渲染框架

  • DOM访问器:浏览器DOM处理优化
  • UI组件树渲染:React/Vue等虚拟DOM比较

八、真实开发案例

1. Java语言内部

javax.lang.model.element.ElementVisitor
Java标准库中用于注解处理的访问者API:

public interface ElementVisitor<R, P> {
    R visit(Element e, P p);
    R visitPackage(PackageElement e, P p);
    R visitType(TypeElement e, P p);
    R visitVariable(VariableElement e, P p);
    R visitExecutable(ExecutableElement e, P p);
    // 其他元素类型
}

编译器使用此接口实现编译时注解处理、代码生成等高级特性。

2. Java字节码工程库

ASM框架:使用访问者模式处理字节码

public class ClassPrinter extends ClassVisitor {
    public ClassPrinter() {
        super(Opcodes.ASM9);
    }
    
    @Override
    public void visit(int version, int access, String name, 
                     String signature, String superName, String[] interfaces) {
        System.out.println(name + " extends " + superName + " {");
    }
    
    @Override
    public MethodVisitor visitMethod(int access, String name, 
                                    String desc, String signature, String[] exceptions) {
        System.out.println("  " + name + desc);
        return null;
    }
    
    @Override
    public void visitEnd() {
        System.out.println("}");
    }
}

// 使用
ClassReader reader = new ClassReader("java.lang.String");
ClassPrinter printer = new ClassPrinter();
reader.accept(printer, 0);

3. Spring框架

Bean定义访问器BeanDefinitionVisitor
在Spring IOC容器内部实现中:

public class CustomBeanDefinitionVisitor extends BeanDefinitionVisitor {
    @Override
    public void visitBeanDefinition(BeanDefinition beanDefinition) {
        // 在Bean定义加载时处理定制逻辑
    }
    
    @Override
    public void visitPropertyValues(PropertyValues pvs) {
        // 处理属性值
    }
}

用于在Bean加载过程中实现定制处理,如配置加密解密。

4. Eclipse建模框架(EMF)

EMF.EList的访问器
在模型驱动开发中:

public class ModelVisitor {
    void visit(EObject obj) {
        for (EStructuralFeature feature : obj.eClass().getEAllStructuralFeatures()) {
            Object value = obj.eGet(feature);
            if (value instanceof EObject) {
                visit((EObject)value);
            } else if (value instanceof EList) {
                ((EList<?>)value).forEach(this::processElement);
            }
        }
    }
}

5. 数据库访问层

SQL AST访问器
在复杂SQL构建器中:

public class SQLExpressionVisitor {
    void visit(SelectStatement stmt) { ... }
    void visit(FromClause from) { ... }
    void visit(WhereClause where) { ... }
    void visit(JoinExpression join) { ... }
}

MyBatis等框架使用此模式解析和优化SQL语句。

九、总结

方面总结
模式类型行为型设计模式
核心意图分离数据结构与操作,允许添加新操作而不改变数据结构
关键角色访问者、具体访问者、元素、具体元素、对象结构
核心机制双重分派(元素accept方法调用访问者visit方法)
主要优点1. 开闭原则(添加新操作容易)
2. 操作相关代码集中
3. 跨元素类累积状态简单
4. 分离无关行为
主要缺点1. 违反封装性
2. 添加新元素困难
3. 可能破坏对象完整性
4. 学习曲线陡峭
适用场景对象结构稳定但操作频繁变化;跨类型元素操作;编译器/AST处理
最佳实践1. 保持对象结构稳定
2. 避免访问者状态污染
3. 使用组合访问者减少遍历
4. 文档化访问者契约
演变应用1. 编译器/解释器设计
2. 数据序列化框架
3. 基础设施自动化
4. UI渲染优化
工业案例1. Java注解处理器
2. ASM字节码操作
3. Spring Bean定义处理
4. SQL AST处理

访问者模式是处理稳定对象结构和变化操作的终极解决方案,尤其在以下场景表现突出:

  • 需要向现有类层次添加新操作
  • 操作需要跨越不同类实现
  • 对象结构复杂但相对稳定

尽管有其局限性(特别是对对象结构变化的敏感性),访问者模式在编译器设计、序列化框架和复杂数据转换等场景中仍是无可替代的强大工具。正确使用访问者模式可以大幅提高系统的可扩展性和可维护性,但需要谨慎权衡其对对象封装性的影响。

在Java生态系统中,访问者模式广泛用于编译器(javac的注解处理)、字节码工程(ASM)和Spring框架的核心基础设施中。理解访问者模式的精髓,将帮助你在面临适当场景时做出更优雅的架构决策。

posted @ 2025-08-30 00:21  NeoLshu  阅读(5)  评论(0)    收藏  举报  来源