大白话说Java设计模式-42-访问者模式(业务实战篇)

📌 PDF :AI人工智能 --- RAG知识库实战项目

大白话说Java设计模式-42-访问者模式(业务实战篇):大白商城营销规则计算的"数据结构与操作分离"

📌 一句话本质:访问者模式就是"把操作从数据结构中分离出来",让数据结构稳定,操作易扩展。

🏷️ 标签:访问者模式 / Java 设计模式 / 数据结构与操作分离 / 营销规则 / 大白商城 🎯 适合:初中级后端 / 经常被"操作扩展"困扰的工程师


目录


一、业务场景引入:为什么需要"操作分离"?

大白商城 2024 年营销中心遇到一个老大难:

"商品 / 订单 / 优惠券 3 种对象,要支持 8 种营销计算(满减/折扣/立减/送积分/送券/限时/拼团/秒杀),每加一种计算要改 3 个类!"

我打开代码一看:

java 复制代码
/**
 * ❌ 大白商城老的营销代码
 */
public class Goods {
    public BigDecimal calculateDiscount(String rule) {
        // ❌ 营销规则耦合在商品类
        if ("FULL_REDUCE".equals(rule)) {
            // 满减逻辑
        } else if ("DISCOUNT".equals(rule)) {
            // 折扣逻辑
        } else if ("INSTANT_REDUCE".equals(rule)) {
            // 立减逻辑
        }
        // ... 500 行
    }
}

public class Order {
    public BigDecimal calculateDiscount(String rule) {
        // ❌ 又复制一遍
        if ("FULL_REDUCE".equals(rule)) { ... }
        // ... 500 行
    }
}

public class Coupon {
    public BigDecimal calculateDiscount(String rule) {
        // ❌ 又复制一遍
        // ... 500 行
    }
}

这种代码的痛点

序号 问题 后果
8 种规则 × 3 个类 = 24 处重复 改 1 处要改 24 处
加新规则改 3 个类 改老代码
业务对象臃肿 商品类 500 行营销逻辑
难测试 业务和营销耦合

老板要的"加新规则只加 1 个类",怎么搞?

答案就是------访问者模式

1.1 大白话讲透访问者

继续打比方:

场景:3 种对象(商品/订单/优惠券)要支持 8 种营销计算。

  • 错误做法:把营销逻辑写在每个对象里
  • 正确做法
    1. 数据结构(商品/订单/优惠券)保持稳定
    2. 每种营销规则封装成独立访问者
    3. 对象接受访问者,让访问者操作自己
    4. 加新规则?只加 1 个访问者类

访问者 = 把操作从数据结构中分离出来,数据结构稳定,操作易扩展

1.2 访问者模式的 3 个真实场景

大白商城里,访问者模式用在:

场景 "元素" "访问者"
营销规则 商品/订单/优惠券 满减/折扣/立减
报表生成 报表节点 PDF/Excel/HTML
编译器 AST 节点 类型检查/代码生成
文件处理 文件节点 压缩/加密/分析

任何"数据结构稳定,操作多变"的场景,都用访问者


二、反面教材:硬编码操作的"灾难现场"

我们看 4 个反面教材,看它们是怎么一步步崩的。

2.1 反面教材 v1:操作写在对象里

java 复制代码
/**
 * ❌ 反面教材 v1:操作写在对象里
 */
public class Goods {
    public BigDecimal calculateFullReduce(...) { ... }
    public BigDecimal calculateDiscount(...) { ... }
    // 8 个方法
}

翻车现场

序号 问题 后果
类臃肿 500 行
加新规则要改类 改老代码

2.2 反面教材 v2:if-else 堆叠

java 复制代码
/**
 * ❌ 反面教材 v2:if-else 堆叠
 */
public BigDecimal calculate(String rule) {
    if ("FULL_REDUCE".equals(rule)) { ... }
    else if ("DISCOUNT".equals(rule)) { ... }
    // 8 个 else if
}

翻车现场

序号 问题 后果
业务臃肿 难维护
加新规则要改方法 改老代码

2.3 反面教材 v3:完全分散

java 复制代码
/**
 * ❌ 反面教材 v3:完全分散
 */
public class GoodsDiscount { ... }
public class GoodsFullReduce { ... }
public class OrderDiscount { ... }
// 8 × 3 = 24 个类

翻车现场

序号 问题 后果
类爆炸 24 个类
重复逻辑 难维护

2.4 反面教材 v4:完全没抽象

java 复制代码
/**
 * ❌ 反面教材 v4:完全没抽象
 */
public class GoodsServiceV4 {
    public void process() {
        // 1000 行所有逻辑
    }
}

翻车现场

序号 问题 后果
无法复用 改一处牵动全身
无法测试 单元测试跑不动

2.5 4 个反面教材的共同病根

痛点 反模式方案能不能解决?
加新规则不动老代码 ❌ 全部要改
数据结构稳定 ❌ 全部臃肿
操作独立测试 ❌ 全部耦合
业务清晰 ❌ 全部混乱

必须上访问者模式


三、模式原理:访问者的"元素 + 访问者"

3.1 访问者的 2 个核心概念

元素(Element)

  • 数据结构
  • 接受访问者
  • 把自己交给访问者处理

访问者(Visitor)

  • 操作实现
  • 对每个元素实现 visit 方法
  • 不修改数据结构

大白商城例子

概念 例子
元素接口 MarketingElement
具体元素 Goods / Order / Coupon
访问者接口 MarketingVisitor
具体访问者 FullReduceVisitor / DiscountVisitor / ...

3.2 访问者的 5 个核心角色

角色 职责 例子
元素接口(Element) 定义 accept MarketingElement
具体元素 A(Concrete Element A) 实现 accept Goods
具体元素 B(Concrete Element B) 实现 accept Order
访问者接口(Visitor) 定义 visit MarketingVisitor
具体访问者(Concrete Visitor) 实现 visit FullReduceVisitor

3.3 一张图看懂访问者

复制代码
MarketingElement(元素接口)
  │
  │ accept(visitor)
  │
  ├── Goods(具体元素)
  │     └── accept(visitor) → visitor.visit(this)
  │
  ├── Order(具体元素)
  │     └── accept(visitor) → visitor.visit(this)
  │
  └── Coupon(具体元素)
        └── accept(visitor) → visitor.visit(this)

MarketingVisitor(访问者接口)
  │
  │ visit(Goods) / visit(Order) / visit(Coupon)
  │
  ├── FullReduceVisitor(满减)
  ├── DiscountVisitor(折扣)
  └── InstantReduceVisitor(立减)

3.4 访问者的"灵魂三问"

Q1:访问者 vs 策略,区别是什么?

答:

  • 访问者数据结构稳定(元素不变)
  • 策略算法独立(任意替换)
  • 访问者作用于多种元素策略作用于单一对象
  • 访问者操作随数据结构变化 ,策略操作固定

Q2:访问者 vs 中介者,区别是什么?

答:

  • 访问者多种操作作用于多种元素
  • 中介者多个对象通过中介者协调
  • 访问者操作外置 ,中介者协调集中
  • 访问者强调"操作",中介者强调"协调"

Q3:访问者模式的核心价值是什么?

答:

  • 数据结构稳定(元素不变)
  • 操作易扩展(加新操作只加访问者)
  • 多种操作作用于多种元素(一次遍历完成多操作)
  • 集中操作(操作都在访问者里)

3.5 访问者的 3 种实现方式

方式 特点 适用
经典访问者 接口 + 双重分派 标准
反射访问者 反射调用 灵活
Lambda 访问者 函数式 简单

大白商城主推经典访问者方式


四、实战代码:大白商城营销规则访问者完整实现

下面是大白商城生产环境在用的访问者实现,全套代码可直接复制到 IDEA 跑

4.1 项目环境与依赖

pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <groupId>com.dabai.mall</groupId>
    <artifactId>mall-design-pattern-42</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>mall-design-pattern-42</name>
    <description>大白商城 - 设计模式 42 访问者模式</description>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.2 元素接口:MarketingElement

java 复制代码
package com.dabai.mall.visitor;

import com.dabai.mall.visitor.visitor.MarketingVisitor;

/**
 * ✅ 元素接口:营销元素
 * <p>
 * 接受访问者
 *
 * @author 大白商城技术团队
 */
public interface MarketingElement {

    /**
     * ✅ 接受访问者
     */
    void accept(MarketingVisitor visitor);
}

4.3 访问者接口:MarketingVisitor

java 复制代码
package com.dabai.mall.visitor.visitor;

import com.dabai.mall.visitor.element.Coupon;
import com.dabai.mall.visitor.element.Goods;
import com.dabai.mall.visitor.element.Order;

/**
 * ✅ 访问者接口:营销访问者
 *
 * @author 大白商城技术团队
 */
public interface MarketingVisitor {

    /**
     * ✅ 访问商品
     */
    void visit(Goods goods);

    /**
     * ✅ 访问订单
     */
    void visit(Order order);

    /**
     * ✅ 访问优惠券
     */
    void visit(Coupon coupon);
}

4.4 3 个具体元素

Goods

java 复制代码
package com.dabai.mall.visitor.element;

import com.dabai.mall.visitor.MarketingElement;
import com.dabai.mall.visitor.visitor.MarketingVisitor;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.math.BigDecimal;

/**
 * ✅ 具体元素:商品
 */
@Data
@AllArgsConstructor
public class Goods implements MarketingElement {

    private Long id;
    private String name;
    private BigDecimal price;
    private Integer stock;

    @Override
    public void accept(MarketingVisitor visitor) {
        // ✅ 把自己交给访问者
        visitor.visit(this);
    }
}

Order

java 复制代码
package com.dabai.mall.visitor.element;

import com.dabai.mall.visitor.MarketingElement;
import com.dabai.mall.visitor.visitor.MarketingVisitor;
import lombok.Data;

import java.math.BigDecimal;
import java.util.List;

/**
 * ✅ 具体元素:订单
 */
@Data
public class Order implements MarketingElement {

    private String orderId;
    private BigDecimal total;
    private List<Goods> items;

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

Coupon

java 复制代码
package com.dabai.mall.visitor.element;

import com.dabai.mall.visitor.MarketingElement;
import com.dabai.mall.visitor.visitor.MarketingVisitor;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.math.BigDecimal;

/**
 * ✅ 具体元素:优惠券
 */
@Data
@AllArgsConstructor
public class Coupon implements MarketingElement {

    private String id;
    private String name;
    private BigDecimal amount;
    private BigDecimal threshold;  // 满减门槛

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

4.5 3 个具体访问者

FullReduceVisitor(满减)

java 复制代码
package com.dabai.mall.visitor.visitor;

import com.dabai.mall.visitor.element.Coupon;
import com.dabai.mall.visitor.element.Goods;
import com.dabai.mall.visitor.element.Order;
import lombok.extern.slf4j.Slf4j;

/**
 * ✅ 具体访问者:满减
 */
@Slf4j
public class FullReduceVisitor implements MarketingVisitor {

    @Override
    public void visit(Goods goods) {
        // ✅ 满 100 减 10
        if (goods.getPrice().compareTo(new java.math.BigDecimal("100")) > 0) {
            log.info("【满减】商品 {} 满 100 减 10, 原价: {}", 
                    goods.getName(), goods.getPrice());
        }
    }

    @Override
    public void visit(Order order) {
        // ✅ 订单满 200 减 20
        if (order.getTotal().compareTo(new java.math.BigDecimal("200")) > 0) {
            log.info("【满减】订单 {} 满 200 减 20, 原总价: {}", 
                    order.getOrderId(), order.getTotal());
        }
    }

    @Override
    public void visit(Coupon coupon) {
        log.info("【满减】优惠券 {} 门槛: {}, 优惠: {}", 
                coupon.getName(), coupon.getThreshold(), coupon.getAmount());
    }
}

DiscountVisitor(折扣)

java 复制代码
package com.dabai.mall.visitor.visitor;

import com.dabai.mall.visitor.element.Coupon;
import com.dabai.mall.visitor.element.Goods;
import com.dabai.mall.visitor.element.Order;
import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;

/**
 * ✅ 具体访问者:折扣
 */
@Slf4j
public class DiscountVisitor implements MarketingVisitor {

    private final BigDecimal discountRate;  // 0.8 = 8 折

    public DiscountVisitor(BigDecimal discountRate) {
        this.discountRate = discountRate;
    }

    @Override
    public void visit(Goods goods) {
        BigDecimal newPrice = goods.getPrice().multiply(discountRate);
        log.info("【折扣】商品 {} {} 折, 原价: {}, 现价: {}", 
                goods.getName(), discountRate, goods.getPrice(), newPrice);
    }

    @Override
    public void visit(Order order) {
        BigDecimal newTotal = order.getTotal().multiply(discountRate);
        log.info("【折扣】订单 {} {} 折, 原总价: {}, 现总价: {}", 
                order.getOrderId(), discountRate, order.getTotal(), newTotal);
    }

    @Override
    public void visit(Coupon coupon) {
        log.info("【折扣】优惠券 {} 享受 {} 折", coupon.getName(), discountRate);
    }
}

InstantReduceVisitor(立减)

java 复制代码
package com.dabai.mall.visitor.visitor;

import com.dabai.mall.visitor.element.Coupon;
import com.dabai.mall.visitor.element.Goods;
import com.dabai.mall.visitor.element.Order;
import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;

/**
 * ✅ 具体访问者:立减
 */
@Slf4j
public class InstantReduceVisitor implements MarketingVisitor {

    private final BigDecimal reduceAmount;

    public InstantReduceVisitor(BigDecimal reduceAmount) {
        this.reduceAmount = reduceAmount;
    }

    @Override
    public void visit(Goods goods) {
        BigDecimal newPrice = goods.getPrice().subtract(reduceAmount);
        log.info("【立减】商品 {} 立减 {}, 原价: {}, 现价: {}", 
                goods.getName(), reduceAmount, goods.getPrice(), newPrice);
    }

    @Override
    public void visit(Order order) {
        BigDecimal newTotal = order.getTotal().subtract(reduceAmount);
        log.info("【立减】订单 {} 立减 {}, 原总价: {}, 现总价: {}", 
                order.getOrderId(), reduceAmount, order.getTotal(), newTotal);
    }

    @Override
    public void visit(Coupon coupon) {
        log.info("【立减】优惠券 {} 立减 {}", coupon.getName(), reduceAmount);
    }
}

4.6 对象结构:MarketingObjectStructure

java 复制代码
package com.dabai.mall.visitor;

import com.dabai.mall.visitor.element.Coupon;
import com.dabai.mall.visitor.element.Goods;
import com.dabai.mall.visitor.element.Order;
import com.dabai.mall.visitor.visitor.MarketingVisitor;
import lombok.extern.slf4j.Slf4j;

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

/**
 * ✅ 对象结构:营销对象结构
 * <p>
 * 持有所有元素,统一接受访问者
 *
 * @author 大白商城技术团队
 */
@Slf4j
public class MarketingObjectStructure {

    private final List<MarketingElement> elements = new ArrayList<>();

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

    /**
     * ✅ 接受访问者:每个元素都接受
     */
    public void accept(MarketingVisitor visitor) {
        log.info("【对象结构】开始接受访问者: {}", 
                visitor.getClass().getSimpleName());
        for (MarketingElement element : elements) {
            element.accept(visitor);
        }
    }
}

4.7 业务调用方

java 复制代码
package com.dabai.mall.service;

import com.dabai.mall.visitor.*;
import com.dabai.mall.visitor.element.*;
import com.dabai.mall.visitor.visitor.*;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;

/**
 * ✅ 营销服务:访问者模式的调用方
 * <p>
 * 业务方只调 accept(visitor),不关心访问者细节
 *
 * @author 大白商城技术团队
 */
@Service
public class MarketingService {

    /**
     * ✅ 业务方只写一种代码
     */
    public void processMarketing() {
        // 1️⃣ 构建对象结构
        MarketingObjectStructure structure = new MarketingObjectStructure();

        Goods goods = new Goods(1L, "iPhone", 
                new BigDecimal("5999"), 100);
        Order order = new Order();
        order.setOrderId("ORDER_001");
        order.setTotal(new BigDecimal("10000"));
        Coupon coupon = new Coupon("CP_001", "满 100 减 10", 
                new BigDecimal("10"), new BigDecimal("100"));

        structure.addElement(goods);
        structure.addElement(order);
        structure.addElement(coupon);

        // 2️⃣ 应用满减规则
        structure.accept(new FullReduceVisitor());

        // 3️⃣ 应用 8 折规则
        structure.accept(new DiscountVisitor(new BigDecimal("0.8")));

        // 4️⃣ 应用立减 50 规则
        structure.accept(new InstantReduceVisitor(new BigDecimal("50")));
    }
}

4.8 单元测试

java 复制代码
package com.dabai.mall.visitor;

import com.dabai.mall.visitor.element.*;
import com.dabai.mall.visitor.visitor.*;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 访问者模式完整单元测试
 */
class MarketingVisitorTest {

    @Test
    void testFullReduceVisitor() {
        MarketingObjectStructure structure = new MarketingObjectStructure();
        Goods goods = new Goods(1L, "iPhone", 
                new BigDecimal("5999"), 100);
        structure.addElement(goods);

        structure.accept(new FullReduceVisitor());
    }

    @Test
    void testDiscountVisitor() {
        MarketingObjectStructure structure = new MarketingObjectStructure();
        Goods goods = new Goods(1L, "iPhone", 
                new BigDecimal("100"), 100);
        structure.addElement(goods);

        structure.accept(new DiscountVisitor(new BigDecimal("0.8")));
    }

    @Test
    void testInstantReduceVisitor() {
        MarketingObjectStructure structure = new MarketingObjectStructure();
        Goods goods = new Goods(1L, "iPhone", 
                new BigDecimal("100"), 100);
        structure.addElement(goods);

        structure.accept(new InstantReduceVisitor(new BigDecimal("20")));
    }

    @Test
    void testMultipleVisitors() {
        // ✅ 一个结构接受多个访问者
        MarketingObjectStructure structure = new MarketingObjectStructure();
        Goods goods = new Goods(1L, "iPhone", 
                new BigDecimal("5999"), 100);
        Order order = new Order();
        order.setOrderId("ORDER_001");
        order.setTotal(new BigDecimal("10000"));
        structure.addElement(goods);
        structure.addElement(order);

        // ✅ 应用 3 种规则
        structure.accept(new FullReduceVisitor());
        structure.accept(new DiscountVisitor(new BigDecimal("0.9")));
        structure.accept(new InstantReduceVisitor(new BigDecimal("50")));
    }

    @Test
    void testEmptyStructure() {
        // ✅ 空结构接受访问者
        MarketingObjectStructure structure = new MarketingObjectStructure();
        structure.accept(new FullReduceVisitor());
    }

    @Test
    void testVisitorExtensibility() {
        // ✅ 加新规则只加 1 个访问者
        MarketingObjectStructure structure = new MarketingObjectStructure();
        Goods goods = new Goods(1L, "iPhone", 
                new BigDecimal("5999"), 100);
        structure.addElement(goods);

        // 加新规则只加新访问者,老代码零改动
        structure.accept(new FullReduceVisitor());
    }
}

五、访问者 vs 策略 / 中介者

5.1 一张表看清区别

模式 关注点 数据结构 操作 适用
访问者 操作分离 稳定 易扩展 数据结构稳定
策略 算法切换 任意 互相独立 多种算法
中介者 多对象协调 多对象 通信 多对象协调

5.2 一个具体例子区分 3 个模式

大白商城

模式 例子
访问者 营销规则作用于商品/订单/优惠券
策略 支付 4 种方式
中介者 订单/支付/库存/物流协调

5.3 决策树

复制代码
数据结构稳定,操作多变?
├── 是
│   └── ✅ 访问者模式
└── 否
    ├── 多种算法可切换?
    │   ├── 是 → ✅ 策略
    │   └── 否
    │       ├── 多对象协调?
    │       │   ├── 是 → ✅ 中介者
    │       │   └── 否 → 普通类

5.4 访问者 vs 策略:易混淆

维度 访问者 策略
关注 数据结构 算法
操作数 多种操作 单一操作
数据结构 稳定 任意
扩展 加访问者 加策略

大白商城选型

场景 选哪个 原因
营销规则 访问者 多种规则 × 多种对象
支付方式 策略 多种算法
订单协调 中介者 多服务协调
数据导出 访问者 多种格式 × 多种对象

六、JDK 编译器 API 怎么用访问者?

6.1 Java Compiler API:访问者经典应用

核心源码

java 复制代码
/**
 * ✅ JDK 访问者:JavaFileObject
 */
public interface JavaFileObject extends FileObject {
    // 标记接口
}

/**
 * ✅ JDK 访问者:SimpleJavaFileObject
 */
public class SimpleJavaFileObject implements JavaFileObject {
    // 接受编译器访问
}

6.2 DiagnosticListener:访问者接口

java 复制代码
/**
 * ✅ 访问者接口
 */
@FunctionalInterface
public interface DiagnosticListener<S> {
    void report(Diagnostic<? extends S> diagnostic);
}

使用

java 复制代码
/**
 * ✅ 业务方
 */
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();

// ✅ 传入访问者
JavaCompiler.CompilationTask task = compiler.getTask(
    null, null, diagnostics, null, null, compilationUnits);

task.call();

// ✅ 访问者被回调
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
    log.info("Error on line: {} - {}", 
            diagnostic.getLineNumber(), diagnostic.getMessage(null));
}

6.3 javax.lang.model.element.ElementVisitor:访问者

核心源码

java 复制代码
/**
 * ✅ JDK 访问者:ElementVisitor
 */
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);
    R visitTypeParameter(TypeParameterElement e, P p);
    R visitUnknown(Element e, P p);
}

6.4 大白商城的"抄作业"

java 复制代码
/**
 * 借鉴 JDK:自定义访问者
 */
public class LoggingVisitor implements MarketingVisitor {

    @Override
    public void visit(Goods goods) {
        log.info("【访问者】商品: id={}, name={}", goods.getId(), goods.getName());
    }

    @Override
    public void visit(Order order) {
        log.info("【访问者】订单: id={}, total={}", 
                order.getOrderId(), order.getTotal());
    }

    @Override
    public void visit(Coupon coupon) {
        log.info("【访问者】优惠券: id={}, amount={}", 
                coupon.getId(), coupon.getAmount());
    }
}

七、ASM / Spring SpEL 怎么借鉴访问者?

7.1 ASM:字节码访问者

核心源码

java 复制代码
/**
 * ✅ ASM 访问者:ClassVisitor
 */
public abstract class ClassVisitor {
    public void visit(int version, int access, String name, ...) { ... }
    public FieldVisitor visitField(...) { ... }
    public MethodVisitor visitMethod(...) { ... }
    public void visitEnd() { ... }
}

/**
 * ✅ 业务方:继承访问者
 */
public class MyClassVisitor extends ClassVisitor {
    @Override
    public MethodVisitor visitMethod(int access, String name, ...) {
        // ✅ 自定义处理
        return super.visitMethod(access, name, ...);
    }
}

使用

java 复制代码
ClassReader reader = new ClassReader(bytes);
ClassWriter writer = new ClassWriter(reader, 0);
ClassVisitor visitor = new MyClassVisitor(writer);
reader.accept(visitor, 0);

7.2 Spring SpEL:表达式访问者

核心源码(简化):

java 复制代码
/**
 * ✅ SpEL 访问者:内部用访问者遍历 AST
 */
public class SpelNode {
    public abstract <T> T accept(SpelNodeVisitor<T> visitor);
}

public interface SpelNodeVisitor<T> {
    T visit(SpelNode node);
}

7.3 大白商城的"抄作业"

java 复制代码
/**
 * 借鉴 ASM:自定义业务访问者
 */
public class StatisticsVisitor implements MarketingVisitor {

    private Map<String, Integer> stats = new HashMap<>();

    @Override
    public void visit(Goods goods) {
        stats.merge("goods", 1, Integer::sum);
    }

    @Override
    public void visit(Order order) {
        stats.merge("order", 1, Integer::sum);
    }

    @Override
    public void visit(Coupon coupon) {
        stats.merge("coupon", 1, Integer::sum);
    }

    public void printStats() {
        log.info("【统计】{}", stats);
    }
}

八、工程决策 Checklist

8.1 ✅ 这 5 种情况,强烈建议用访问者

序号 场景 原因
数据结构稳定 商品/订单/优惠券
操作多变 营销规则
多种操作作用于多种对象 报表/导出
需要遍历复杂对象 AST / XML
操作需独立 满减/折扣/立减

8.2 ❌ 这 5 种情况,绝对不要用访问者

序号 场景 原因
数据结构常变 加元素改所有访问者
业务简单 过度设计
操作少 不值得
追求简单 类多
对象结构简单 不需要

8.3 ⚠️ 访问者的 6 大常见坑

序号 表现 解决方案
数据结构变动 改 1 个类动 N 个访问者 谨慎
破坏封装 访问者要访问内部 提供接口
双重分派难理解 学习曲线 文档
性能 visit 链慢 缓存
循环引用 栈溢出 检测
类型硬编码 编译期耦合 反射

8.4 面试官视角:访问者高频追问

Q1:访问者 vs 策略,区别是什么?

答:访问者数据结构稳定 (元素不变),策略算法独立访问者作用于多种元素,策略作用于单一对象

Q2:访问者模式的核心价值是什么?

答:数据结构稳定 + 操作易扩展 。加新操作只加访问者,元素不变适用于操作多变、数据结构稳定的场景

Q3:访问者模式最大的缺点是什么?

答:难增加新元素 。加新元素要改所有访问者。所以访问者适用于"数据结构稳定"的场景


九、与其他模式协作

9.1 访问者 + 组合 = 树形结构访问

java 复制代码
/**
 * ✅ 访问者 + 组合:树形结构
 */
public class TreeNode implements MarketingElement {
    private List<MarketingElement> children = new ArrayList<>();
    public void accept(MarketingVisitor visitor) {
        visitor.visit(this);
        children.forEach(c -> c.accept(visitor));
    }
}

9.2 访问者 + 迭代器 = 遍历 + 操作

9.3 访问者 + 策略 = 多种访问者策略

9.4 大白商城模式协作全景图

复制代码
                    ┌──────────────┐
                    │  访问者      │ ← 本篇
                    └──────┬───────┘
                           │
       ┌───────────┬───────┼───────┬───────────┐
       │           │       │       │           │
   ┌───▼───┐  ┌────▼───┐ ┌▼────┐ ┌▼─────┐  ┌───▼────┐
   │组合   │  │迭代器  │ │策略 │ │单例  │  │ 工厂   │
   │(树)   │  │(遍历) │ │(操作)│ │(访问)│  │ 方法   │
   └───────┘  └────────┘ └──────┘ └──────┘  └───────┘
   20 篇       38 篇     26 篇     02 篇     04 篇

十、本篇小结 + 下篇预告

10.1 本篇小结(5 个核心要点)

  1. 本质 :访问者 = 数据结构与操作分离加新操作只加访问者
  2. 场景 :营销规则 / 报表生成 / 编译器 / 文件处理,操作多变
  3. 对比:访问者 vs 策略(多种 vs 单一)/ 访问者 vs 中介者(操作外置 vs 协调集中)。
  4. JDK Compiler APIElementVisitor + DiagnosticListener 经典应用。
  5. 避坑:数据结构变动 / 破坏封装 / 双重分派 / 性能问题。

10.2 一句话总结

访问者不是"换种方式写业务",是"用数据结构稳定、操作易扩展的思路" 。大白商城从 24 处重复 → 3 个元素 + 3 个访问者------加新营销规则只加 1 个访问者,商品/订单/优惠券类零改动

10.3 知识脑图

复制代码
访问者模式
├── 2 个核心概念
│   ├── 元素(Element)
│   └── 访问者(Visitor)
├── 5 大角色
│   ├── 元素接口(MarketingElement)
│   ├── 具体元素(3 个)
│   ├── 访问者接口(MarketingVisitor)
│   ├── 具体访问者(3 个)
│   └── 对象结构(MarketingObjectStructure)
├── 3 个具体元素
│   ├── Goods
│   ├── Order
│   └── Coupon
├── 3 个具体访问者
│   ├── FullReduceVisitor(满减)
│   ├── DiscountVisitor(折扣)
│   └── InstantReduceVisitor(立减)
├── 模式对比
│   ├── vs 策略(多种 vs 单一)
│   ├── vs 中介者(操作 vs 协调)
│   └── vs 迭代器(操作 vs 遍历)
├── JDK Compiler API
│   ├── JavaFileObject
│   ├── DiagnosticListener
│   └── ElementVisitor
└── 模式协作
    ├── + 组合(树形访问)
    ├── + 迭代器(遍历+操作)
    └── + 策略(多种访问者)

10.4 下篇预告

第 43 篇【访问者模式 - 源码剖析篇】:JDK / Spring 中的访问者实现

下一篇我们会深入源码,回答三个问题:

  1. JDK 的 ElementVisitor 怎么用访问者遍历 AST?
  2. ASM 的 ClassVisitor 怎么访问字节码?
  3. Spring SpEL 怎么用访问者求值?

并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
观测云18 分钟前
Spring AI + Spring AI Alibaba 接入观测云最佳实践
人工智能·spring
步行cgn19 分钟前
传统 SSM 与 Spring Boot 开发对比:从配置地狱到约定优于配置
java·spring boot·后端
ttwosix26 分钟前
JavaEE初阶 多线程:单例模式 | 阻塞队列
java·开发语言
zcmodeltech1 小时前
垃圾发电厂沙盘模型控制系统设计与实现:多设备协同联动方案
java·大数据·数据库·人工智能·stm32·嵌入式硬件·制造
AI人工智能+电脑小能手1 小时前
大白话说Java设计模式-43-访问者模式(源码剖析篇)
java·设计模式·访问者模式·源码分析·elementvisitor·asm classvisitor·spring spel
丘山一郎10 小时前
Spring 中的IOC控制反转 和DI依赖注入
java·后端·spring
码视野10 小时前
基于 Spring Boot + Vue3 的【大学英语四六级 (CET-4/6) 作文智能评分与句式润色系统】设计与实现(含PRD/三端高保真源码/大屏)
java·前端·人工智能·spring boot·后端·vue3
MetaLite10 小时前
Spring-AOP自调用为什么失效-AopContext真能解决吗
java·后端·spring
大模型码小白10 小时前
AI 对话流性能调优:万级消息的虚拟滚动落地
java·大数据·前端·javascript·人工智能·算法·机器学习