Java 23 种设计模式:从踩坑到精通 | 番外:访问者模式 —— 购物车结算实战

摘要:访问者模式表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。通过“双重分派”机制,访问者模式将操作从数据结构中分离出来,让新增操作不再需要修改元素类。本文结合电商购物车结算的场景,完整展示如何用访问者模式将散落在各元素中的称重逻辑集中到访问者中,并深入讲解双分派机制、与迭代器模式的对比,帮你掌握“操作与结构分离”的设计精髓。

🗺️ 本文阅读地图(3 分钟速览)

  • 为什么给购物车增加一种“操作”要改所有商品类?
  • 访问者核心角色:访问者、具体访问者、元素、对象结构
  • 手写购物车称重:书籍和水果统一由访问者处理
  • 双分派:访问者模式的灵魂机制
  • 面试必问:“访问者模式为什么要求元素类稳定?什么是双分派?”

📖 《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 正篇:Visitor 访问者模式 —— 数据结构稳定但操作多变?试试访问者 | 当前:番外 · 访问者模式 × 购物车结算
🔗 返回系列总目录


1. 购物车结算的痛点

在电商系统中,购物车里有多种商品——书籍、水果、电子产品等。每种商品需要支持多种操作:称重统计、折扣计算、打包方式等。如果直接在商品类中写这些操作:

class Book {
    void calculateWeight() { /* 书籍称重逻辑 */ }
    void calculateDiscount() { /* 书籍折扣逻辑 */ }
    void pack() { /* 书籍打包逻辑 */ }
}

class Fruit {
    void calculateWeight() { /* 水果称重逻辑 */ }
    void calculateDiscount() { /* 水果折扣逻辑 */ }
    void pack() { /* 水果打包逻辑 */ }
}

这种方式导致商品类职责膨胀——每新增一种操作(如“环保包装费”),就要修改所有商品类。更麻烦的是,同一种操作的逻辑散落在不同商品类中,难以统一管理和优化。

访问者模式的解决思路:将“操作”从商品类中分离出来,封装为独立的访问者对象。商品类只保留数据和 accept() 方法,称重逻辑集中到 ConcreteVisitor 中。新增操作只需新增一个访问者类,商品类零修改。

1.1 你的场景该不该用访问者?

判断标准 是 → 用访问者 否 → 用其他方式
数据结构稳定(元素类型很少变化)
需要频繁新增对数据结构的操作
操作之间互不相关,需要独立管理
数据结构频繁变化(经常新增元素类型) 直接在元素类中加方法

2. 访问者模式 UML(购物车结算场景)

在这里插入图片描述


3. 完整源码实现

3.1 抽象元素接口 (Element)

/**
 * 抽象元素:定义接受访问者的接口
 */
public interface Element {
    /**
     * 接受访问者,将自身传递给访问者处理
     */
    void accept(Visitor visitor);
}

💬 白话:所有商品——书籍、水果——都必须能“接待”访问者。accept() 是访问者模式的入口,商品在方法内部把自己交给访问者。

3.2 具体元素A:书籍 (ElementA)

/**
 * 具体元素A:书籍
 */
public class ElementA implements Element {
    private String name;
    private double weight;

    public ElementA(String name, double weight) {
        this.name = name;
        this.weight = weight;
    }

    @Override
    public void accept(Visitor visitor) {
        // 核心:将 this 传递给访问者,由访问者决定如何处理
        visitor.visit(this);
    }

    public void operationA() {
        System.out.println("书籍 [" + name + "] 正在执行阅读操作...");
    }

    public String getName() { return name; }
    public double getWeight() { return weight; }
}

💬 白话:书籍的 accept() 只有一行——visitor.visit(this)。书籍不关心访问者要做什么(称重还是打折),只管把自己交出去。

3.3 具体元素B:水果 (ElementB)

/**
 * 具体元素B:水果
 */
public class ElementB implements Element {
    private String name;
    private double weight;

    public ElementB(String name, double weight) {
        this.name = name;
        this.weight = weight;
    }

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

    public void operationB() {
        System.out.println("水果 [" + name + "] 正在执行食用操作...");
    }

    public String getName() { return name; }
    public double getWeight() { return weight; }
}

3.4 抽象访问者接口 (Visitor)

/**
 * 抽象访问者:为每种具体元素声明访问操作
 */
public interface Visitor {
    void visit(ElementA elementA);
    void visit(ElementB elementB);
}

💬 白话:访问者必须为每种商品类型准备一个 visit() 方法。这是 Java 的方法重载——方法名相同但参数类型不同。

3.5 具体访问者:购物车称重统计 (ConcreteVisitor)

/**
 * 具体访问者:购物车称重统计
 * 将原本散落在各元素中的称重逻辑集中于此
 */
public class ConcreteVisitor implements Visitor {
    private double totalWeight = 0.0;

    @Override
    public void visit(ElementA elementA) {
        System.out.println("正在称重书籍: " + elementA.getName());
        totalWeight += elementA.getWeight();
    }

    @Override
    public void visit(ElementB elementB) {
        System.out.println("正在称重水果: " + elementB.getName());
        totalWeight += elementB.getWeight();
    }

    public void printTotalWeight() {
        System.out.println("=================================");
        System.out.println("🛒 购物车商品总重量: " + totalWeight + " kg");
        System.out.println("=================================");
    }
}

💬 白话:称重逻辑全部集中在这里——书籍有自己的称重方式,水果有自己的称重方式。商品类完全不知道称重是怎么回事。如果要新增“打折计算”,只需再写一个 DiscountVisitor,商品类零修改。

3.6 对象结构:购物车 (ObjectStructure)

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

/**
 * 对象结构:管理元素集合,提供统一访问入口
 */
public class ObjectStructure {
    private List<Element> elements = new ArrayList<>();

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

    /**
     * 遍历所有元素,让访问者依次访问
     */
    public void accept(Visitor visitor) {
        for (Element element : elements) {
            element.accept(visitor);
        }
    }
}

💬 白话:购物车负责“管理商品集合”,它遍历所有商品,让访问者逐个处理。商品和访问者都不需要知道对方具体是谁。

3.7 客户端测试

public class Client {
    public static void main(String[] args) {
        // 1. 构建对象结构
        ObjectStructure cart = new ObjectStructure();
        cart.addElement(new ElementA("《Java 23种设计模式》", 1.2));
        cart.addElement(new ElementB("红富士苹果", 2.5));
        cart.addElement(new ElementA("《深入理解JVM》", 1.5));
        cart.addElement(new ElementB("海南香蕉", 1.8));

        // 2. 创建具体访问者
        ConcreteVisitor visitor = new ConcreteVisitor();

        // 3. 触发访问
        cart.accept(visitor);

        // 4. 输出结果
        visitor.printTotalWeight();
    }
}

4. 运行结果

正在称重书籍: 《Java 23种设计模式》
正在称重水果: 红富士苹果
正在称重书籍: 《深入理解JVM》
正在称重水果: 海南香蕉
=================================
🛒 购物车商品总重量: 7.0 kg
=================================

5. 核心角色回顾

角色 职责 对应代码
Visitor 为每种具体元素声明访问操作 Visitor
ConcreteVisitor 集中实现每种元素的具体操作 ConcreteVisitor
Element 定义接受访问者的接口 Element
ConcreteElement 实现 accept(),将自己传给访问者 ElementA / ElementB
ObjectStructure 管理元素集合,统一入口 ObjectStructure

6. 双分派机制详解

访问者模式的核心是双分派(Double Dispatch)——通过两次方法调用,同时确定“哪个访问者”和“哪种元素”:

// 第一次分派(客户端):确定用哪个访问者
element.accept(visitor);

// 第二次分派(元素内部):确定元素的真实类型
public void accept(Visitor visitor) {
    visitor.visit(this);   // this 的类型是确定的:ElementA 或 ElementB
}
调用步骤 分派内容 决定因素
element.accept(visitor) 选择哪个访问者 客户端传入的具体访问者类型
visitor.visit(this) 选择 visit 的哪个重载版本 this 的具体元素类型

💬 白话:第一次分派是“你要做什么操作”(称重还是打折),第二次分派是“你要对什么商品做操作”(书籍还是水果)。Java 本身是单分派语言,访问者模式通过两次单分派模拟了双分派效果。

7. 访问者模式 vs 迭代器模式

对比项 访问者模式 迭代器模式
目的 在数据结构上执行操作 顺序访问元素
操作归属 操作封装在访问者中 操作由客户端执行
扩展方向 操作维度(新增访问者) 遍历方式(新增迭代器)
典型应用 ASM 字节码、报表生成 集合遍历

💡 一句话记忆:迭代器解决“怎么遍历”,访问者解决“遍历时做什么”。两者可以组合使用——用迭代器遍历集合,在遍历中对每个元素应用访问者。

8. 访问者模式的优缺点

优点 缺点
新增操作无需修改元素类,符合开闭原则 新增元素类型需修改访问者接口及所有实现
相同操作集中管理,便于维护 需要元素暴露部分内部状态,破坏封装
访问者可以累计状态(如总重量) 双分派机制理解成本高

9. 六大设计原则体现

原则 体现
单一职责 元素管数据,访问者管操作
开闭原则 新增操作只需新增访问者(新增元素则违反)
里氏替换 所有元素可替换 Element,所有访问者可替换 Visitor
依赖倒置 元素依赖抽象 Visitor,访问者依赖抽象 Element
接口隔离 Visitor 为每种元素定义独立的 visit() 方法
迪米特法则 对象结构封装遍历逻辑,客户端只与访问者交互

附 访问者模式 UML源码(购物车结算场景)

@startuml
title Java 23 种设计模式:从踩坑到精通
footer 折哥 | 智能物流与Java实战

' 1. 全局样式配置
skinparam backgroundColor #FEFEFE
skinparam shadowing false
skinparam classBorderColor #333333
skinparam classFontColor #1A1A1A
skinparam classFontSize 14
skinparam noteFontSize 12
skinparam noteFontColor #555555
skinparam arrowColor #555555
skinparam classBackgroundColor #F9F9F9
skinparam interface {
    BackgroundColor #E8F5E9
    BorderColor #2E7D32
}

' 2. 抽象访问者
interface Visitor {
    + visit(ElementA)
    + visit(ElementB)
}
note right of Visitor
  <b>抽象访问者</b>
  --
  为每种具体元素声明访问操作
  新增元素时需修改此接口
end note

' 3. 具体访问者
class ConcreteVisitor implements Visitor {
    + visit(ElementA)
    + visit(ElementB)
}
note right of ConcreteVisitor
  <b>具体访问者</b>
  --
  实现每种元素的具体操作
  将散落在元素中的逻辑集中于此
end note

' 4. 抽象元素
interface Element {
    + accept(Visitor)
}
note right of Element
  <b>抽象元素</b>
  --
  定义接受访问者的接口
  是连接元素与访问者的桥梁
end note

' 5. 具体元素A
class ElementA implements Element {
    + accept(Visitor)
    + operationA()
}

' 6. 具体元素B
class ElementB implements Element {
    + accept(Visitor)
    + operationB()
}

' 7. 对象结构
class ObjectStructure {
    - elements : List<Element>
    + accept(Visitor)
}
note right of ObjectStructure
  <b>对象结构</b>
  --
  管理元素集合
  提供统一入口让访问者遍历
end note

' 8. 关系连线
ObjectStructure o-down-> Element : 包含
ConcreteVisitor .up.> Visitor : 实现
ElementA .up.> Element : 实现
ElementB .up.> Element : 实现

@enduml

🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦 福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。

📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》《电商多平台电子面单对接实战》。技术相通,思路可鉴。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐