设计模式——访问者设计模式(行为型)

news2025/7/28 5:31:22

摘要

访问者设计模式是一种行为型设计模式,它将数据结构与作用于结构上的操作解耦,允许在不修改数据结构的前提下增加新的操作行为。该模式包含关键角色如元素接口、具体元素类、访问者接口和具体访问者类。通过访问者模式,可以在不改变对象结构的情况下,定义新的操作行为。文章通过示例场景和类图、时序图等详细介绍了访问者设计模式的结构和实现方式,并探讨了其适用场景和实战示例。

1. 访问者设计模式定义

将数据结构与作用于结构上的操作解耦,使得在不修改数据结构的前提下,可以增加新的操作行为。访问者模式允许你在不改变对象结构(如树、图、元素集合)的前提下,定义新的操作行为,通过将这些操作封装到独立的 "访问者" 对象中。

1.1. 关键角色说明

角色

说明

Element

定义 accept(Visitor)接口,让访问者访问自己。

ConcreteElement

实现 Element接口,实现接受访问者的具体逻辑。

Visitor

抽象访问者,定义访问每个元素的接口 visit(ElementA)等。

ConcreteVisitor

实现 Visitor,封装对元素结构的具体操作,比如导出、统计、渲染等行为。

1.2. 示例场景(通俗类比)

假设你有一个对象结构为:公司组织结构,每个节点可以是 员工部门。你希望在不修改员工、部门类的前提下,分别实现:

  • 统计薪资总额
  • 导出组织结构为 HTML
  • 打印汇报关系图

通过访问者模式,你可以创建多个 ConcreteVisitor 来实现上述功能,而无需改动 Element 本身代码

2. 访问者设计模式结构

  1. 访问者 (Visitor) 接口声明了一系列以对象结构的具体元素为参数的访问者方法。 如果编程语言支持重载, 这些方法的名称可以是相同的, 但是其参数一定是不同的。
  2. 具体访问者 (Concrete Visitor) 会为不同的具体元素类实现相同行为的几个不同版本。
  3. 元素 (Element) 接口声明了一个方法来 “接收” 访问者。 该方法必须有一个参数被声明为访问者接口类型。
  4. 具体元素 (Concrete Element) 必须实现接收方法。 该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。 请注意, 即使元素基类实现了该方法, 所有子类都必须对其进行重写并调用访问者对象中的合适方法。
  5. 客户端 (Client) 通常会作为集合或其他复杂对象 (例如一个组合(opens new window)树) 的代表。 客户端通常不知晓所有的具体元素类, 因为它们会通过抽象接口与集合中的对象进行交互。

2.1. 访问者设计模式类图

2.2. 访问者设计模式时序图

3. 访问者设计模式实现方式

访问者设计模式的实现方式,核心在于:将作用于对象结构的操作行为封装到独立的访问者类中,并通过 accept(Visitor) 方法把访问者“注入”到元素中,从而实现对结构中不同元素的不同处理。

3.1. 步骤 1:定义元素接口 Element

public interface Element {
    void accept(Visitor visitor);
}

3.2. 步骤 2:实现具体元素类(ConcreteElement)

public class Employee implements Element {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    // 提供访问者访问自己
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    // getter
    public String getName() { return name; }
    public double getSalary() { return salary; }
}
public class Department implements Element {
    private String deptName;

    public Department(String deptName) {
        this.deptName = deptName;
    }

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

    public String getDeptName() { return deptName; }
}

3.3. 步骤 3:定义访问者接口 Visitor

public interface Visitor {
    void visit(Employee employee);
    void visit(Department department);
}

3.4. 步骤 4:实现具体访问者(ConcreteVisitor)

public class ReportVisitor implements Visitor {
    @Override
    public void visit(Employee employee) {
        System.out.println("员工:" + employee.getName() + ",薪资:" + employee.getSalary());
    }

    @Override
    public void visit(Department department) {
        System.out.println("部门:" + department.getDeptName());
    }
}

3.5. 步骤 5:使用访问者

public class Client {
    public static void main(String[] args) {
        List<Element> elements = Arrays.asList(
            new Employee("张三", 12000),
            new Employee("李四", 15000),
            new Department("风控部")
        );

        Visitor visitor = new ReportVisitor();

        for (Element element : elements) {
            element.accept(visitor);  // 每个元素调用 visitor.visit(this)
        }
    }
}

3.6. 说明总结

点位

描述

解耦操作和结构

不改动 Element 的结构代码,就能添加任意多种访问逻辑。

遵循开闭原则

新增操作时,只需新建访问者类即可。

单一职责更清晰

每个访问者只关注自己的行为。

缺点:扩展结构麻烦

每次新增 Element 子类,所有 Visitor都要加新的 visit() 方法。

4. 访问者设计模式适合场景

以下是访问者(Visitor)设计模式适合不适合使用的场景清单,便于你快速判断在实战开发中是否应当使用此模式。

4.1. ✅ 适合使用访问者设计模式的场景

场景

说明

需要对对象结构中不同元素进行不同操作

如处理复杂对象树时,不同节点类型需要不同处理(如 AST 抽象语法树、HTML DOM、组织结构等)。

需要在不修改类的前提下增加新操作

元素类稳定,但经常添加新功能,适合将操作逻辑外移成访问者类。符合开闭原则。

多个操作跨多个类共享处理逻辑

如统计报表、导出功能、数据校验,每种功能可封装为访问者,避免元素类职责膨胀。

数据结构较复杂,逻辑需要分离

尤其在组合模式(Composite)中遍历树状结构时,访问者是理想搭档。

需要记录访问轨迹/数据收集/行为链式执行

访问者可收集上下文数据,实现功能链、审计等操作。

4.2. ❌ 不适合使用访问者设计模式的场景

场景

原因

对象结构不稳定,经常增删元素类型

每新增一个元素类,所有访问者都必须修改,违反开闭原则,维护成本高。

操作种类少,变化不频繁

如果只有一两种操作,直接放到元素类中即可,访问者反而引入了额外复杂性。

操作需要频繁访问元素内部状态/私有字段

访问者访问元素的内部字段时会暴露实现细节,可能破坏封装性。

数据驱动而非行为驱动系统

如果处理逻辑更多是对数据表进行规则驱动处理,不如使用策略模式、责任链、状态机等。

系统对性能极度敏感,层层函数调用不可接受

访问者调用链过长,对性能要求极高的系统中不推荐使用。

4.3. ✅ 实战使用建议

建议点

内容

👍 推荐与组合模式搭配使用

树形结构遍历 + 多种处理逻辑,非常适合访问者模式。

👍 可与责任链、模板方法组合

在访问者中执行链式操作或分步骤逻辑。

⚠️ 避免与频繁变更的领域模型搭配

如果业务模型变化频繁,访问者维护成本非常高。

5. 访问者设计模式实战示例

以下是一个基于访问者设计模式的 Spring 项目实战示例,应用于金融风控场景,使用注解方式注入对象,涵盖了完整的结构。

金融风控中,需要对不同类型的用户(例如:个人、企业)进行多维度风险评估,比如信用评分、交易行为分析、黑名单检查等。不同用户类型暴露的数据结构不同,但我们希望将“风险评估逻辑”从数据结构中解耦出来。

使用访问者模式可实现:

  • 新增风险评估逻辑(访问者)无需修改用户结构;
  • 用户数据结构与评估操作解耦,符合开闭原则。

5.1. 📦 项目结构

risk-visitor
├── model
│   ├── User.java
│   ├── PersonalUser.java
│   └── CompanyUser.java
├── visitor
│   ├── RiskVisitor.java
│   ├── CreditScoreVisitor.java
│   └── FraudCheckVisitor.java
├── config
│   └── VisitorConfig.java
└── RiskEvaluationService.java

5.2. 用户对象层(Element)

public interface User {
    void accept(RiskVisitor visitor);
}
@Data
public class PersonalUser implements User {
    private String name;
    private String idCard;
    private int creditScore;

    @Override
    public void accept(RiskVisitor visitor) {
        visitor.visit(this);
    }
}
@Data
public class CompanyUser implements User {
    private String companyName;
    private String licenseNumber;
    private double registeredCapital;

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

5.3. 风控访问者接口与实现

public interface RiskVisitor {
    void visit(PersonalUser personalUser);
    void visit(CompanyUser companyUser);
}

5.3.1. 信用评分访问者

@Component
public class CreditScoreVisitor implements RiskVisitor {

    @Override
    public void visit(PersonalUser personalUser) {
        System.out.println("[信用评分] 用户 " + personalUser.getName() + " 分数: " + personalUser.getCreditScore());
    }

    @Override
    public void visit(CompanyUser companyUser) {
        System.out.println("[信用评分] 公司 " + companyUser.getCompanyName() + " 注册资本: " + companyUser.getRegisteredCapital());
    }
}

5.3.2. 欺诈检测访问者

@Component
public class FraudCheckVisitor implements RiskVisitor {

    @Override
    public void visit(PersonalUser personalUser) {
        System.out.println("[欺诈检查] 检查身份证是否黑名单:" + personalUser.getIdCard());
    }

    @Override
    public void visit(CompanyUser companyUser) {
        System.out.println("[欺诈检查] 检查营业执照是否异常:" + companyUser.getLicenseNumber());
    }
}

5.4. 风控服务类(注解注入访问者)

@Service
public class RiskEvaluationService {

    private final List<RiskVisitor> visitors;

    @Autowired
    public RiskEvaluationService(List<RiskVisitor> visitors) {
        this.visitors = visitors;
    }

    public void evaluate(User user) {
        for (RiskVisitor visitor : visitors) {
            user.accept(visitor);
        }
    }
}

5.5. 启动与使用

@SpringBootApplication
public class RiskApp implements CommandLineRunner {

    @Autowired
    private RiskEvaluationService evaluationService;

    @Override
    public void run(String... args) {
        PersonalUser personalUser = new PersonalUser();
        personalUser.setName("张三");
        personalUser.setIdCard("123456789");
        personalUser.setCreditScore(750);

        CompanyUser companyUser = new CompanyUser();
        companyUser.setCompanyName("风控科技");
        companyUser.setLicenseNumber("XYZ-2025");
        companyUser.setRegisteredCapital(5000_000);

        evaluationService.evaluate(personalUser);
        evaluationService.evaluate(companyUser);
    }

    public static void main(String[] args) {
        SpringApplication.run(RiskApp.class, args);
    }
}

5.6. ✅ 总结优点

  • 易于扩展新评估策略,无需改动用户结构;
  • Spring 自动注入访问者集合,灵活组合;
  • 清晰分离了数据结构与操作行为。

6. 访问者设计模式思考

访问者设计模式(Visitor Pattern)在实际开发中常常与其他设计模式组合使用,以增强系统的可扩展性、解耦能力和灵活性。下面列出访问者模式常与哪些设计模式组合使用,以及各组合的典型应用场景和优势

6.1. ✅ 访问者模式常用组合设计模式

组合模式

组合目的/优势

应用场景示例

组合模式(Composite)

用于遍历和访问复杂对象结构,访问者可递归处理整个树形结构

文档结构、组织架构、产品分类树等

迭代器模式(Iterator)

统一遍历容器结构,配合访问者实现对集合中元素的操作(如批量处理)

批量风控评估、设备监控列表操作

责任链模式(Chain of Responsibility)

多个访问者对象串联处理,解耦多个处理逻辑,每个访问者判断是否处理

多规则风控审批流程,每个处理节点负责一类校验

模板方法模式(Template Method)

访问者中封装处理通用流程,将子类特定行为抽象为钩子方法

通用风险评估框架,子类定义评分规则

策略模式(Strategy)

将访问者作为策略进行注入或切换,使不同访问行为可配置化

不同国家/行业的风险评估策略

状态模式(State)

被访问对象的状态决定了访问者逻辑流程,用于基于状态执行不同操作

用户行为轨迹、风控状态迁移等

工厂模式(Factory)

访问者工厂根据上下文动态创建访问者对象,适配不同对象结构或执行策略

动态风控策略调度系统,按对象类型或场景创建访问者

观察者模式(Observer)

访问者中执行完后通知监听者,适用于监控、日志、审计等异步行为

风控决策日志记录、报警事件触发

博文参考

  • 访问者设计模式
  • 设计模式之访问者模式 | DESIGN

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2398723.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实验设计与分析(第6版,Montgomery著,傅珏生译) 第10章拟合回归模型10.9节思考题10.1 R语言解题

本文是实验设计与分析&#xff08;第6版&#xff0c;Montgomery著&#xff0c;傅珏生译) 第10章拟合回归模型10.9节思考题10.1 R语言解题。主要涉及线性回归、回归的显著性、回归系数的置信区间。 vial <- seq(1, 10, 1) Viscosity <- c(160,171,175,182,184,181,188,19…

LeetCode 高频 SQL 50 题(基础版) 之 【高级查询和连接】· 下

上部分链接&#xff1a;LeetCode 高频 SQL 50 题&#xff08;基础版&#xff09; 之 【高级查询和连接】 上 题目&#xff1a;1164. 指定日期的产品价格 题解&#xff1a; select product_id,10 price from Products group by product_id having min(change_date) > 201…

机器学习——SVM

1.什么是SVM 支持向量机&#xff08;support vector machines&#xff0c;SVM&#xff09;是一种二分类模型&#xff0c;它将实例的特征向量映射为空间中的一些点&#xff0c;SVM 的目的就是想要画出一条线&#xff0c;以 “最好地” 区分这两类点&#xff0c;以至如果以后有了…

【音视频】FFmpeg 硬件(NVDIA)编码H264

FFmpeg 与x264的关系 ffmpeg软编码是使⽤x264开源项⽬&#xff0c;也就是说ffmpeg软编码H264最终是调⽤了x264开源项⽬&#xff0c;所以我们要先理解ffmpeg和x264的调⽤关系&#xff0c;这⾥我们主要关注x264_init。对于x264的参数都在 ffmpeg\libavcodec \libx264.c x264\co…

贪心算法应用:超图匹配问题详解

贪心算法应用&#xff1a;超图匹配问题详解 贪心算法在超图匹配问题中有着广泛的应用。下面我将从基础概念到具体实现&#xff0c;全面详细地讲解超图匹配问题及其贪心算法解决方案。 一、超图匹配问题基础 1. 超图基本概念 **超图&#xff08;Hypergraph&#xff09;**是普…

【Web应用】若依框架:基础篇13 源码阅读-前端代码分析

文章目录 ⭐前言⭐一、课程讲解过程⭐二、自己动手实操⭐总结 标题详情作者JosieBook头衔CSDN博客专家资格、阿里云社区专家博主、软件设计工程师博客内容开源、框架、软件工程、全栈&#xff08;,NET/Java/Python/C&#xff09;、数据库、操作系统、大数据、人工智能、工控、网…

[java八股文][JavaSpring面试篇]SpringCloud

了解SpringCloud吗&#xff0c;说一下他和SpringBoot的区别 Spring Boot是用于构建单个Spring应用的框架&#xff0c;而Spring Cloud则是用于构建分布式系统中的微服务架构的工具&#xff0c;Spring Cloud提供了服务注册与发现、负载均衡、断路器、网关等功能。 两者可以结合…

基于分布式状态机的集装箱智能道口软件架构方法

集装箱码头对进出场道口的通过能力始终是要求最高的&#xff0c;衡量道口的直接指标为道口通行效率&#xff0c;道口通行效率直接体现了集装箱码头的作业效率以及对外服务水平&#xff0c;进而直接影响到码头的综合能力。所以&#xff0c;码头普遍使用智能道口实现24小时无人值…

Vue 树状结构控件

1、效果图如下所示&#xff1a; 2、网络请求的数据结构如下&#xff1a; 3、新建插件文件&#xff1a;menu-tree.vue&#xff0c;插件代码如下&#xff1a; <template><div class"root"><div class"parent" click"onParentClick(pare…

【LUT技术专题】图像自适应3DLUT代码讲解

本文是对图像自适应3DLUT技术的代码解读&#xff0c;原文解读请看图像自适应3DLUT文章讲解 1、原文概要 结合3D LUT和CNN&#xff0c;使用成对和非成对的数据集进行训练&#xff0c;训练后能够完成自动的图像增强&#xff0c;同时还可以做到极低的资源消耗。下图为整个模型的…

vscode使用“EIDE”和“Cortex-Debug”插件利用st-link插件实现程序烧写以及调试工作

第一步&#xff1a;安装vscode插件“EIDE”EIDE和“Cortex-Debug”。 第二步&#xff1a;配置EIDE 2.1安装“实用工具”&#xff1a; 2.2 EIDE插件配置&#xff1a;根据安装的keil C51 keil MDK IAR的相关路径设置 第三步&#xff1a;配置Cortex-Debug插件 点击settings.jso…

Spring @Value注解的依赖注入实现原理

Spring Value注解的依赖注入实现原理 一&#xff0c;什么是Value注解的依赖注入二&#xff0c;实现原理三&#xff0c;代码实现1. 定义 Value 注解2. 实现 InstantiationAwareBeanPostProcessor3. 实现 AutowiredAnnotationBeanPostProcessor4. 占位符解析逻辑5. 定义 StringVa…

三、kafka消费的全流程

五、多线程安全问题 1、多线程安全的定义 使用多线程访问一个资源&#xff0c;这个资源始终都能表现出正确的行为。 不被运行的环境影响、多线程可以交替访问、不需要任何额外的同步和协同。 2、Java实现多线程安全生产者 这里只是模拟多线程环境下使用生产者发送消息&…

Axure形状类组件图标库(共8套)

点击下载《月下倚楼图标库(形状组件)》 原型效果&#xff1a;https://axhub.im/ax9/02043f78e1b4386f/#g1 摘要 本图标库集锦精心汇集了8套专为Axure设计的形状类图标资源&#xff0c;旨在为产品经理、UI/UX设计师以及开发人员提供丰富多样的设计素材&#xff0c;提升原型设计…

20250530-C#知识:String与StringBuilder

String与StringBuilder string字符串在开发中经常被用到&#xff0c;不过在需要频繁对字符串进行增加和删除时&#xff0c;使用StringBuilder有利于提升效率。 1、String string是一种引用类型而非值类型&#xff08;某些方面像值类型&#xff09;使用“”进行两个string对象的…

从 Docker 到 Containerd:Kubernetes 容器运行时迁移实战指南

一、背景 Kubernetes 自 v1.24 起移除了 dockershim&#xff0c;不再原生支持 Docker Engine&#xff0c;用户需迁移至受支持的 CRI 兼容运行时&#xff0c;如&#xff1a; Containerd&#xff08;推荐&#xff0c;高性能、轻量级&#xff09; CRI-O&#xff08;专为 Kuberne…

uniapp中view标签使用范围

不止用于微信小程序。兼容型号&#xff0c;是uniapp内置组件之一&#xff0c;在uniapp中进行了跨平台适配。支持所有uniapp的平台。如微信小程序、h5、app、支付宝小程序

欢乐熊大话蓝牙知识14:用 STM32 或 EFR32 实现 BLE 通信模块:从0到蓝牙,你也能搞!

&#x1f680; 用 STM32 或 EFR32 实现 BLE 通信模块&#xff1a;从0到蓝牙&#xff0c;你也能搞&#xff01; “我能不能自己用 STM32 或 EFR32 实现一个 BLE 模块&#xff1f;” 答案当然是&#xff1a;能&#xff01;还能很帅&#xff01; &#x1f468;‍&#x1f3ed; 前…

IDEA 在公司内网配置gitlab

赋值项目链接 HTTPS 将HTTP的链接 ip地址换成 内网地址 例如&#xff1a;https:172.16.100.18/...... 如果出现需要需要Token验证的情况&#xff1a; 参考&#xff1a;Idea2024中拉取代码时GitLab提示输入token的问题_gitlab token-CSDN博客

黑马Java面试笔记之 微服务篇(业务)

一. 限流 你们项目中有没有做过限流?怎么做的? 为什么要限流呢? 一是并发的确大(突发流量) 二是防止用户恶意刷接口 限流的实现方式: Tomcat:可以设置最大连接数 可以通过maxThreads设置最大Tomcat连接数,实现限流,但是适用于单体架构 Nginx:漏桶算法网关,令牌桶算法自定…