访问者模式(Visitor Pattern)详解


一、访问者模式简介

访问者模式(Visitor Pattern) 是一种 行为型设计模式(对象行为型模式),它允许你在不修改对象结构的前提下,为对象结构中的元素添加新的操作。

你可以这样理解:

“有一个公司组织结构图(包含多个部门、员工),现在你想分别计算工资总额、打印员工名单、生成报表等不同功能。如果每次都要修改每个类来支持新功能,会非常麻烦。而访问者模式就像请来不同的‘专家’(访问者)——一个财务专家算工资,一个人事专家做花名册——他们各自去‘访问’每个员工并完成自己的任务。”

核心思想是:将数据结构与作用于结构上的操作分离。

它为操作存储不同类型元素的对象结构提供了一种解决方案。
用户可以对不同类型的元素施加不同的操作。

访问者模式包含以下5个角色:
Visitor(抽象访问者)
ConcreteVisitor(具体访问者)
Element(抽象元素)
ConcreteElement(具体元素)
ObjectStructure(对象结构)

在这里插入图片描述


二、解决的问题类型

访问者模式主要用于解决以下问题:

  • 需要对一个复杂的对象结构(如树、列表)执行多种不同的操作,且这些操作可能会频繁增加。
  • 不想修改现有类来添加新功能(避免破坏封装性或违反开闭原则)。
  • 希望将相关操作集中在一个类中(即访问者),而不是分散在各个数据类中。

三、使用场景

场景示例
编译器设计抽象语法树(AST)的解析、类型检查、代码生成等
文档处理系统对文档中的段落、图片、表格进行渲染、导出PDF、统计字数等
UI组件树操作遍历组件树进行布局、绘制、事件分发等
报表生成对一组对象进行汇总、分析、生成统计报告

一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。
需要对一个对象结构中的对象进行很多不同的且不相关的操作,并需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。
对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。


四、核心概念

  1. Visitor(访问者接口):定义对每种元素的访问方法,如 visit(ElementA)、visit(ElementB)。
  2. ConcreteVisitor(具体访问者):实现访问者接口,完成具体操作(如打印、计算、导出等)。
  3. Element(元素接口):定义 accept(Visitor) 方法,用于接收访问者。
  4. ConcreteElement(具体元素):实现 accept 方法,调用访问者的对应 visit 方法。
  5. ObjectStructure(对象结构):如集合、树等,能枚举元素并让访问者遍历它们。

五、实际代码案例(Java)

我们以一个“文档编辑器”为例,文档包含文本段落和图片,我们需要实现“渲染”和“导出为HTML”两种操作。

1. 定义访问者接口

// 访问者接口
interface DocumentVisitor {
    void visit(Paragraph paragraph);
    void visit(Image image);
}

2. 定义元素接口

// 元素接口
interface DocumentElement {
    void accept(DocumentVisitor visitor);
}

3. 创建具体元素类

// 段落元素
class Paragraph implements DocumentElement {
    private String content;

    public Paragraph(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }

    @Override
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this); // 反向调用访问者,传入自己
    }
}

// 图片元素
class Image implements DocumentElement {
    private String url;

    public Image(String url) {
        this.url = url;
    }

    public String getUrl() {
        return url;
    }

    @Override
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this);
    }
}

4. 创建具体访问者类

// 渲染访问者
class RenderVisitor implements DocumentVisitor {
    @Override
    public void visit(Paragraph paragraph) {
        System.out.println("🖥️ 渲染段落: " + paragraph.getContent());
    }

    @Override
    public void visit(Image image) {
        System.out.println("🖼️ 渲染图片: [显示图片 " + image.getUrl() + "]");
    }
}

// 导出为HTML访问者
class HtmlExportVisitor implements DocumentVisitor {
    private StringBuilder html = new StringBuilder();

    @Override
    public void visit(Paragraph paragraph) {
        html.append("<p>").append(paragraph.getContent()).append("</p>\n");
    }

    @Override
    public void visit(Image image) {
        html.append("<img src=\"").append(image.getUrl()).append("\" />\n");
    }

    public String getHtml() {
        return html.toString();
    }
}

5. 创建对象结构(文档)

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

// 文档结构(对象结构)
class Document {
    private List<DocumentElement> elements = new ArrayList<>();

    public void addElement(DocumentElement element) {
        elements.add(element);
    }

    // 接受访问者遍历所有元素
    public void accept(DocumentVisitor visitor) {
        for (DocumentElement element : elements) {
            element.accept(visitor);
        }
    }
}

6. 客户端测试类

public class Client {
    public static void main(String[] args) {
        Document doc = new Document();
        doc.addElement(new Paragraph("欢迎使用我们的文档系统。"));
        doc.addElement(new Image("https://example.com/logo.png"));
        doc.addElement(new Paragraph("这是一个示例文档。"));

        System.out.println("=== 渲染文档 ===");
        RenderVisitor renderVisitor = new RenderVisitor();
        doc.accept(renderVisitor);

        System.out.println("\n=== 导出为HTML ===");
        HtmlExportVisitor htmlVisitor = new HtmlExportVisitor();
        doc.accept(htmlVisitor);
        System.out.println(htmlVisitor.getHtml());
    }
}

输出结果:

=== 渲染文档 ===
🖥️ 渲染段落: 欢迎使用我们的文档系统。
🖼️ 渲染图片: [显示图片 https://example.com/logo.png]
🖥️ 渲染段落: 这是一个示例文档。

=== 导出为HTML ===
<p>欢迎使用我们的文档系统。</p>
<img src="https://example.com/logo.png" />
<p>这是一个示例文档。</p>

典型代码

典型的抽象访问者类代码:

abstract class Visitor
{
	public abstract void Visit(ConcreteElementA elementA);
	public abstract void Visit(ConcreteElementB elementB);
	public void Visit(ConcreteElementC elementC)
	{
		//元素ConcreteElementC的操作代码
	}
}

典型的具体访问者类代码:

class ConcreteVisitor : Visitor
{
public override void Visit(ConcreteElementA elementA) 
    {
	    //元素ConcreteElementA的操作代码
    }
    public override void Visit(ConcreteElementB elementB) 
    {
	    //元素ConcreteElementB的操作代码
    }
}

典型的抽象元素类代码:

interface Element
{
    void Accept(Visitor visitor);
}

典型的具体元素类代码:

class ConcreteElementA : Element 
{
	public void Accept(Visitor visitor) 
	{
		visitor.Visit(this);
	}
		
	public void OperationA() 
	{
		//业务方法
	}
}

典型的对象结构代码:

using System;
using System.Collections.Generic;
class ObjectStructure
{
    private List<Element> list = new List<Element>(); //定义一个集合用于存储元素对象
//接受访问者的访问操作
public void Accept(Visitor visitor)
{
foreach (Object obj in list)
        {
 ((Element)obj).Accept(visitor); //遍历访问集合中的每一个元素
}
}
    public void AddElement(Element element)
    {
        list.Add(element);
    }
public void RemoveElement(Element element)
    {
        list.Remove(element);
    }
}

访问者模式的结构与实现
双重分派机制
(1) 调用具体元素类的Accept(Visitor visitor)方法,并将Visitor子类对象作为其参数
(2) 在具体元素类Accept(Visitor visitor)方法内部调用传入的Visitor对象的Visit()方法,例如Visit(ConcreteElementA elementA),将当前具体元素类对象(this)作为参数,例如visitor.Visit(this)
(3) 执行Visitor对象的Visit()方法,在其中还可以调用具体元素对象的业务方法

ConcreteElementA. Accept(Visitor visitor)
                  ↓
ConcreteVisitorA. Visit(ConcreteElementA elementA)
	<ConcreteVisitorA. Visit(this)>
                  ↓
ConcreteElementA. OperationA()

其他案例

  1. 某公司OA系统中包含一个员工信息管理子系统,该公司员工包括正式员工和临时工,每周人力资源部和财务部等部门需要对员工数据进行汇总,汇总数据包括员工工作时间、员工工资等。该公司基本制度如下:
    (1) 正式员工每周工作时间为40小时,不同级别、不同部门的员工每周基本工资不同;如果超过40小时,超出部分按照100元/小时作为加班费;如果少于40小时,所缺时间按照请假处理,请假所扣工资以80元/小时计算,直到基本工资扣除到零为止。除了记录实际工作时间外,人力资源部需记录加班时长或请假时长,作为员工平时表现的一项依据。
    (2) 临时工每周工作时间不固定,基本工资按小时计算,不同岗位的临时工小时工资不同。人力资源部只需记录实际工作时间。
    人力资源部和财务部工作人员可以根据各自的需要对员工数据进行汇总处理,人力资源部负责汇总每周员工工作时间,而财务部负责计算每周员工工资。
    现使用访问者模式设计该系统,绘制类图。

在这里插入图片描述

  1. 购物车
    顾客在超市中将选择的商品,如苹果、图书等放在购物车中,然后到收银员处付款。在购物过程中,顾客需要对这些商品进行访问,以便确认这些商品的质量,之后收银员计算价格时也需要访问购物车内顾客所选择的商品。此时,购物车作为一个ObjectStructure(对象结构)用于存储各种类型的商品,而顾客和收银员作为访问这些商品的访问者,他们需要对商品进行检查和计价。不同类型的商品其访问形式也可能不同,如苹果需要过秤之后再计价,而图书不需要。使用访问者模式来设计该购物过程。

在这里插入图片描述

  1. 奖励审批系统
    某高校奖励审批系统可以实现教师奖励和学生奖励的审批(AwardCheck),如果教师发表论文数超过10篇或者学生论文超过2篇可以评选科研奖,如果教师教学反馈分大于等于90分或者学生平均成绩大于等于90分可以评选成绩优秀奖,使用访问者模式设计该系统,以判断候选人集合中的教师或学生是否符合某种获奖要求。
    在这里插入图片描述

设计结构
在这里插入图片描述


六、优缺点分析

优点描述
✅ 符合开闭原则新增操作(访问者)无需修改现有元素类
✅ 职责分离将相关操作集中到访问者类中,提高内聚性
✅ 便于扩展新操作添加新功能只需新增一个访问者类
缺点描述
❌ 增加新元素类困难每新增一个元素类型,所有访问者都要修改接口并实现新方法(违反开闭原则)
❌ 破坏封装性访问者可能需要访问元素的内部状态
❌ 代码复杂度高引入较多类和双向调用,理解成本较高
❌ 性能开销多态调用和递归可能导致性能下降

七、与其它模式对比

模式与访问者模式的区别
策略模式策略是替换算法,访问者是扩展操作
观察者模式观察者是事件通知,访问者是主动遍历
迭代器模式迭代器用于遍历,访问者用于操作+遍历

八、最终小结

访问者模式是一种强大但使用场景有限的设计模式,它特别适合那些数据结构相对稳定,但需要在其上执行多种不同操作的系统。

作为一名 Java 开发工程师,你可能会在以下场景中遇到它:

  • 编译器、解释器等语言处理工具;
  • 复杂的数据模型需要多种展示或处理方式;
  • 报表、导出、统计等横切功能较多的系统。

但也要注意:如果元素类型经常变化,访问者模式会变得难以维护。因此,它更适合“操作多、结构稳”的场景。


📌 一句话总结:

访问者模式就像“外聘专家团队”,他们带着各自的工具(操作),去访问公司里的各个部门(元素),完成专业任务,而无需改变公司原有结构。


✅ 建议使用时机:

  • 对象结构稳定,但操作频繁扩展;
  • 多种操作需要集中管理;
  • 愿意接受一定的代码复杂度换取灵活性。

以上内部部分由AI大模型生成,注意识别!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐