访问者模式:分离数据结构与操作的设计模式

访问者模式:分离数据结构与操作的设计模式

一、模式核心:将操作从数据结构中分离,支持动态添加新操作

在软件开发中,当数据结构(如树、集合)中的元素类型固定,但需要频繁添加新的操作(如遍历、统计、打印)时,直接修改元素类会违反开闭原则。

访问者模式(Visitor Pattern) 将数据结构与作用于结构上的操作分离,使得操作可以独立于数据结构进行扩展。通过引入访问者(Visitor)角色,客户端可以在不修改数据结构的前提下,新增对数据的操作。核心解决:

  • 操作扩展问题:新增操作时无需修改数据结构类,符合开闭原则。
  • 职责分离:数据结构仅负责存储数据,操作逻辑集中在访问者类中。
  • 复杂操作封装:将跨元素的复杂操作(如树的深度优先遍历 + 统计)封装在访问者中,避免污染数据结构。

核心思想与 UML 类图(PlantUML 语法)

访问者模式包含以下角色:

  1. 抽象访问者(Visitor):定义对每种元素的访问接口。
  2. 具体访问者(Concrete Visitor):实现抽象访问者接口,定义对具体元素的操作。
  3. 抽象元素(Element) :定义接受访问者的接口(accept(Visitor))。
  4. 具体元素(Concrete Element):实现接受访问者的方法,调用访问者的对应操作。
  5. 对象结构(Object Structure):管理元素集合,允许访问者遍历所有元素。

二、核心实现:员工数据统计与可视化

1. 定义抽象元素(员工)

java 复制代码
public interface Employee {  
    void accept(Visitor visitor); // 接受访问者  
    String getName();  
    double getSalary();  
}  

2. 实现具体元素(全职员工、兼职员工)

全职员工(有奖金)
java 复制代码
public class FullTimeEmployee implements Employee {  
    private String name;  
    private double baseSalary;  
    private double bonus;  

    public FullTimeEmployee(String name, double baseSalary, double bonus) {  
        this.name = name;  
        this.baseSalary = baseSalary;  
        this.bonus = bonus;  
    }  

    @Override  
    public void accept(Visitor visitor) {  
        visitor.visitFullTimeEmployee(this); // 调用访问者的对应方法  
    }  

    // Getter 方法  
    public String getName() { return name; }  
    public double getSalary() { return baseSalary + bonus; }  
    public double getBonus() { return bonus; }  
}  
兼职员工(按小时计费)
java 复制代码
public class PartTimeEmployee implements Employee {  
    private String name;  
    private double hourlyRate;  
    private int hoursWorked;  

    public PartTimeEmployee(String name, double hourlyRate, int hoursWorked) {  
        this.name = name;  
        this.hourlyRate = hourlyRate;  
        this.hoursWorked = hoursWorked;  
    }  

    @Override  
    public void accept(Visitor visitor) {  
        visitor.visitPartTimeEmployee(this); // 调用访问者的对应方法  
    }  

    // Getter 方法  
    public String getName() { return name; }  
    public double getSalary() { return hourlyRate * hoursWorked; }  
    public int getHoursWorked() { return hoursWorked; }  
}  

3. 定义抽象访问者(统计与打印)

java 复制代码
public interface Visitor {  
    // 访问全职员工  
    void visitFullTimeEmployee(FullTimeEmployee employee);  
    // 访问兼职员工  
    void visitPartTimeEmployee(PartTimeEmployee employee);  
}  

4. 实现具体访问者(薪资统计、员工信息打印)

薪资统计访问者
java 复制代码
public class SalaryVisitor implements Visitor {  
    private double totalSalary = 0;  

    @Override  
    public void visitFullTimeEmployee(FullTimeEmployee employee) {  
        totalSalary += employee.getSalary();  
        System.out.println("全职员工 " + employee.getName() + " 薪资:" + employee.getSalary());  
    }  

    @Override  
    public void visitPartTimeEmployee(PartTimeEmployee employee) {  
        totalSalary += employee.getSalary();  
        System.out.println("兼职员工 " + employee.getName() + " 薪资:" + employee.getSalary());  
    }  

    public double getTotalSalary() {  
        return totalSalary;  
    }  
}  
员工信息打印访问者
java 复制代码
public class InfoPrintVisitor implements Visitor {  
    @Override  
    public void visitFullTimeEmployee(FullTimeEmployee employee) {  
        System.out.println("全职员工信息:");  
        System.out.println("姓名:" + employee.getName());  
        System.out.println("基本工资:" + employee.getSalary() - employee.getBonus());  
        System.out.println("奖金:" + employee.getBonus());  
    }  

    @Override  
    public void visitPartTimeEmployee(PartTimeEmployee employee) {  
        System.out.println("兼职员工信息:");  
        System.out.println("姓名:" + employee.getName());  
        System.out.println("工作时长:" + employee.getHoursWorked() + " 小时");  
        System.out.println("时薪:" + employee.getHourlyRate());  
    }  
}  

5. 对象结构(员工列表)

java 复制代码
import java.util.ArrayList;  
import java.util.List;  

public class EmployeeManager {  
    private List<Employee> employees = new ArrayList<>();  

    public void addEmployee(Employee employee) {  
        employees.add(employee);  
    }  

    // 接受访问者,遍历所有员工  
    public void accept(Visitor visitor) {  
        for (Employee employee : employees) {  
            employee.accept(visitor);  
        }  
    }  
}  

6. 客户端使用访问者模式

java 复制代码
public class ClientDemo {  
    public static void main(String[] args) {  
        EmployeeManager manager = new EmployeeManager();  
        manager.addEmployee(new FullTimeEmployee("Alice", 8000, 2000));  
        manager.addEmployee(new PartTimeEmployee("Bob", 50, 160));  

        // 统计薪资  
        SalaryVisitor salaryVisitor = new SalaryVisitor();  
        manager.accept(salaryVisitor);  
        System.out.println("\n总薪资:" + salaryVisitor.getTotalSalary());  

        // 打印员工信息  
        InfoPrintVisitor printVisitor = new InfoPrintVisitor();  
        manager.accept(printVisitor);  
    }  
}  

输出结果

plaintext 复制代码
全职员工 Alice 薪资:10000.0  
兼职员工 Bob 薪资:8000.0  

总薪资:18000.0  

全职员工信息:  
姓名:Alice  
基本工资:8000.0  
奖金:2000.0  
兼职员工信息:  
姓名:Bob  
工作时长:160 小时  
时薪:50.0  

三、进阶:双分派与类型安全

访问者模式通过双分派(Double Dispatch) 实现类型安全的操作:

  1. 第一分派 :元素调用 accept(visitor) 时,根据元素类型确定调用哪个 accept 实现。
  2. 第二分派 :在 accept 方法中,调用 visitor.visitXXX(this),根据访问者类型和元素类型确定具体操作。
java 复制代码
// 元素的 accept 方法(第一分派)  
public class FullTimeEmployee implements Employee {  
    @Override  
    public void accept(Visitor visitor) {  
        visitor.visitFullTimeEmployee(this); // 第二分派:传入当前元素实例  
    }  
}  

四、框架与源码中的访问者实践

1. Java 编译器(如 Eclipse JDT)

Java 编译器使用访问者模式遍历抽象语法树(AST),实现词法分析、语法分析和语义分析。例如,ASTVisitor 类定义了对各种节点(如 MethodDeclarationVariableDeclaration)的访问方法。

2. XML/JSON 解析器

解析器将 XML 节点(如 <user><order>)作为元素,访问者实现对节点的验证、转换或统计操作(如验证订单节点的金额是否合法)。

3. Apache Ant 构建工具

Ant 通过访问者模式遍历构建文件(build.xml)中的任务节点(如 <copy><delete>),执行对应的构建操作。

五、避坑指南:正确使用访问者模式的 3 个要点

1. 数据结构稳定性优先

访问者模式适用于数据结构稳定、操作频繁变化的场景。若数据结构经常新增元素类型,需修改所有访问者,违反开闭原则。

2. 避免过度使用双分派

双分派会增加代码复杂度,需确保访问者与元素的类型组合不会导致类爆炸(如元素类型 n,访问者类型 m,需实现 n×m 个方法)。

3. 处理跨元素操作的一致性

若访问者需要维护跨元素的状态(如统计总和),需在访问者中设计状态管理逻辑,避免状态泄漏到数据结构中。

六、总结:何时该用访问者模式?

适用场景 核心特征 典型案例
数据结构固定,操作多变 元素类型不变,但需频繁新增操作 编译器优化、文档格式转换(如 Markdown 转 HTML)
跨元素复杂操作 操作需要遍历多种元素并执行不同逻辑 电商订单统计(同时处理商品、物流、支付元素)
操作与数据解耦 操作逻辑与数据存储分离,独立维护 财务系统(数据存储在数据库,操作包含报表生成、审计)

访问者模式通过分离数据与操作,为系统提供了强大的扩展能力。下一篇我们将探讨最后一个设计模式 ------ 解释器模式,解析如何实现自定义语言的解释器,敬请期待!

扩展思考:访问者模式 vs 策略模式

类型 核心差异 适用场景
访问者模式 操作针对不同元素类型(多态分派) 数据结构固定,操作动态扩展
策略模式 操作是同接口的不同实现(算法切换) 同一元素的不同处理策略
相关推荐
int型码农3 小时前
数据结构第八章(一) 插入排序
c语言·数据结构·算法·排序算法·希尔排序
怀旧,3 小时前
【数据结构】6. 时间与空间复杂度
java·数据结构·算法
积极向上的向日葵3 小时前
有效的括号题解
数据结构·算法·
Java 技术轻分享4 小时前
《树数据结构解析:核心概念、类型特性、应用场景及选择策略》
数据结构·算法·二叉树··都差速
chao_7895 小时前
链表题解——两两交换链表中的节点【LeetCode】
数据结构·python·leetcode·链表
曦月逸霜6 小时前
第34次CCF-CSP认证真题解析(目标300分做法)
数据结构·c++·算法
蔡蓝6 小时前
设计模式-建造者模式
服务器·设计模式·建造者模式
吴声子夜歌9 小时前
OpenCV——Mat类及常用数据结构
数据结构·opencv·webpack
笑口常开xpr9 小时前
数 据 结 构 进 阶:哨 兵 位 的 头 结 点 如 何 简 化 链 表 操 作
数据结构·链表·哨兵位的头节点
@我漫长的孤独流浪10 小时前
数据结构测试模拟题(4)
数据结构·c++·算法