Spring + 设计模式 (十九) 行为型 - 访问者模式

访问者模式

引言

访问者模式是一种行为型设计模式,旨在将数据结构与操作分离,通过引入独立的访问者对象来处理不同类型的元素,而无需修改元素类本身。想象一个博物馆,展品(数据结构)固定,但不同导游(访问者)可以根据自己的专长为游客讲解不同的故事。访问者模式的核心在于将算法与对象结构解耦,支持在不更改类结构的情况下动态添加新操作。它特别适合处理复杂对象结构(如树或集合),在编译期类型已知但操作需灵活扩展的场景中大放异彩。通过将操作外包给访问者,代码变得更模块化、可扩展,但也可能增加复杂度。

实际开发中的用途

访问者模式在实际开发中适用于需要对复杂对象结构执行多种不同操作,且这些操作可能频繁变更的场景。它解决了直接在类中添加操作导致的代码膨胀和维护难题,主要应用场景包括:

  • 复杂对象结构处理:如抽象语法树(AST)解析、JSON/XML 文档处理,需对不同节点执行不同操作。
  • 报表生成:对数据结构(如订单、用户信息)执行统计、格式化或导出操作。
  • 跨模块操作:不同模块需对同一对象结构执行特定逻辑,避免污染核心类。

示例场景:在一个电商系统中,订单包含多种类型的子项(如商品订单、服务订单)。财务部门需要计算总价,物流部门需要生成配送计划。如果在订单类中直接实现这些逻辑,类会变得臃肿且难以维护。使用访问者模式,可以定义价格计算访问者和配送计划访问者,分别处理不同订单类型的逻辑,保持订单类简洁且易于扩展。

访问者模式的优势在于支持开放封闭原则(OCP),新操作只需新增访问者类,无需修改现有类。但它要求元素类型相对稳定,因为新增元素类型需要修改所有访问者,适合结构固定但操作多变的场景。

Spring 源码中的应用

在 Spring 框架中,访问者模式的一个典型应用体现在 Spring Expression Language (SpEL) 的表达式解析与求值过程中。SpEL 使用访问者模式遍历和处理表达式语法树(AST),将解析、求值等操作与节点结构解耦。

源码分析

org.springframework.expression.spel.ast 包中的 SpelNodeSpelNodeVisitor 为例,SpEL 的表达式节点(如 LiteralPropertyOrFieldReference)通过访问者模式支持不同的求值逻辑。以下是关键源码片段,摘自 Spring Framework 5.3.18(SpelNode.javaSpelNodeVisitor.java):

java 复制代码
// SpelNode 接口(部分)
public interface SpelNode {
    // 接受访问者并触发访问
    void accept(SpelNodeVisitor visitor);
    
    // 获取节点值
    Object getValue(ExpressionState expressionState) throws EvaluationException;
}

// SpelNodeVisitor 接口(部分)
public interface SpelNodeVisitor {
    void visit(SpelNode node);
}

// PropertyOrFieldReference 节点(示例节点实现)
public class PropertyOrFieldReference extends SpelNodeImpl {
    private final String name;

    public PropertyOrFieldReference(boolean nullSafe, String name, int startPos, int endPos) {
        super(startPos, endPos);
        this.name = name;
        this.nullSafe = nullSafe;
    }

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

    @Override
    public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
        // 属性求值逻辑
        return ...;
    }
}

// 求值访问者的实现(SpelEvaluationVisitor 示例)
public class SpelEvaluationVisitor implements SpelNodeVisitor {
    private final ExpressionState expressionState;

    public SpelEvaluationVisitor(ExpressionState expressionState) {
        this.expressionState = expressionState;
    }

    @Override
    public void visit(SpelNode node) {
        if (node instanceof PropertyOrFieldReference) {
            PropertyOrFieldReference ref = (PropertyOrFieldReference) node;
            // 执行属性求值
            evaluateProperty(ref);
        } else if (node instanceof Literal) {
            // 处理字面量
        }
        // 其他节点类型
    }
}

分析

  • 访问者角色SpelNodeVisitor 接口及其实现(如 SpelEvaluationVisitor)充当访问者,定义了对不同节点类型的处理逻辑(如求值、编译)。
  • 元素角色SpelNode 的实现类(如 PropertyOrFieldReferenceLiteral)是元素,接受访问者并触发 visit 方法。
  • 解耦效果:节点类只负责提供数据结构(如属性名、字面值),而求值、编译等操作由访问者实现。这种设计允许 SpEL 在不修改节点类的情况下支持新的操作(如类型检查、代码生成)。
  • 问题解决 :SpEL 的 AST 可能包含多种节点类型(如变量、方法调用、运算符)。访问者模式让 SpEL 能够灵活处理这些节点,支持动态表达式求值、编译等功能,广泛应用于 @Value 注解、条件表达式等场景。
  • 代码体现acceptvisit 方法是访问者模式的核心,节点通过 accept 调用访问者的 visit,实现操作与结构的分离。

这种设计使得 SpEL 既灵活又可扩展,开发者可以自定义访问者来实现新的表达式处理逻辑,而无需触碰核心 AST 结构。

Spring Boot 代码案例

以下是一个基于 Spring Boot的案例,模拟一个报表生成系统,使用访问者模式处理不同类型的订单项,生成财务报表和物流计划。

案例背景

在一个电商系统中,订单包含商品订单和服务订单。财务模块需要计算总价,物流模块需要生成配送计划。使用访问者模式,订单项结构保持不变,财务和物流逻辑通过访问者实现。

代码实现

java 复制代码
// 访问者接口
public interface OrderItemVisitor {
    void visit(ProductOrderItem productItem);
    void visit(ServiceOrderItem serviceItem);
}

// 订单项接口
public interface OrderItem {
    void accept(OrderItemVisitor visitor);
}

// 商品订单项
public class ProductOrderItem implements OrderItem {
    private String productName;
    private double price;
    private double weight;

    public ProductOrderItem(String productName, double price, double weight) {
        this.productName = productName;
        this.price = price;
        this.weight = weight;
    }

    public double getPrice() {
        return price;
    }

    public double getWeight() {
        return weight;
    }

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

// 服务订单项
public class ServiceOrderItem implements OrderItem {
    private String serviceName;
    private double fee;

    public ServiceOrderItem(String serviceName, double fee) {
        this.serviceName = serviceName;
        this.fee = fee;
    }

    public double getFee() {
        return fee;
    }

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

// 财务访问者(计算总价)
public class FinancialVisitor implements OrderItemVisitor {
    private double totalPrice = 0;

    @Override
    public void visit(ProductOrderItem productItem) {
        totalPrice += productItem.getPrice();
    }

    @Override
    public void visit(ServiceOrderItem serviceItem) {
        totalPrice += serviceItem.getFee();
    }

    public double getTotalPrice() {
        return totalPrice;
    }
}

// 物流访问者(生成配送计划)
public class LogisticsVisitor implements OrderItemVisitor {
    private double totalWeight = 0;
    private List<String> deliveryItems = new ArrayList<>();

    @Override
    public void visit(ProductOrderItem productItem) {
        totalWeight += productItem.getWeight();
        deliveryItems.add("Product: " + productItem.getProductName());
    }

    @Override
    public void visit(ServiceOrderItem serviceItem) {
        // 服务订单无需配送
    }

    public String getDeliveryPlan() {
        return "Delivery items: " + deliveryItems + ", Total weight: " + totalWeight;
    }
}

// 订单服务
@Service
public class OrderService {
    public Map<String, Object> processOrder(List<OrderItem> items) {
        FinancialVisitor financialVisitor = new FinancialVisitor();
        LogisticsVisitor logisticsVisitor = new LogisticsVisitor();

        for (OrderItem item : items) {
            item.accept(financialVisitor);
            item.accept(logisticsVisitor);
        }

        Map<String, Object> result = new HashMap<>();
        result.put("totalPrice", financialVisitor.getTotalPrice());
        result.put("deliveryPlan", logisticsVisitor.getDeliveryPlan());
        return result;
    }
}

// 测试控制器
@RestController
@RequiredArgsConstructor
public class OrderController {
    private final OrderService orderService;

    @PostMapping("/orders/process")
    public Map<String, Object> processOrder() {
        List<OrderItem> items = Arrays.asList(
            new ProductOrderItem("Laptop", 1000.0, 2.5),
            new ServiceOrderItem("Warranty", 100.0)
        );
        return orderService.processOrder(items);
    }
}

代码说明

  • 访问者模式体现OrderItemVisitor 接口定义了访问逻辑,FinancialVisitorLogisticsVisitor 分别处理价格计算和配送计划。订单项(ProductOrderItemServiceOrderItem)通过 accept 方法接受访问者,操作与结构完全解耦。
  • 优势
    • 高扩展性:新增操作(如税务计算)只需实现新的访问者,无需修改订单项类。
    • 单一职责:订单项类只负责数据,访问者负责操作,符合 SRP 原则。
    • 企业级适用性:案例模拟了报表生成场景,适用于复杂数据结构的处理,支持模块化开发。
  • 运行效果 :通过 POST 请求 /orders/process,返回总价和配送计划(如 {"totalPrice":1100.0, "deliveryPlan":"Delivery items: [Product: Laptop], Total weight: 2.5"})。

相似的设计模式对比

访问者模式与 策略模式(Strategy Pattern)在某些场景下有相似之处,但它们的侧重点和实现方式不同。以下是对比分析:

  • 访问者模式
    • 关键词:数据与操作分离、复杂对象结构、动态扩展操作。
    • 说明:通过访问者处理不同类型的元素,适合结构固定但操作多变的场景,强调操作与结构的解耦。
  • 策略模式
    • 关键词:算法替换、行为封装、动态切换。
    • 说明:定义一系列算法,封装为独立类,允许在运行时切换算法,适合单一职责的算法选择。

对比表格

特性 访问者模式 策略模式
核心思想 分离数据结构与操作 封装可互换的算法
适用场景 复杂对象结构,操作频繁变更 单一职责,算法动态切换
耦合度 较高,新增元素需修改访问者 较低,算法独立且易扩展
代码复杂度 较高,需定义访问者和元素接口 较低,算法类独立实现
Spring 中的实现 SpEL 表达式解析 缓存策略、事务管理器选择

代码对比

以下通过一个简单的价格计算功能对比两者的实现差异。

访问者模式实现
java 复制代码
// 访问者接口
public interface PriceVisitor {
    void visit(Book book);
    void visit(Electronics electronics);
}

// 元素接口
public interface Item {
    void accept(PriceVisitor visitor);
}

// 书类
public class Book implements Item {
    private double price;

    public Book(double price) {
        this.price = price;
    }

    public double getPrice() {
        return price;
    }

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

// 电子产品类
public class Electronics implements Item {
    private double price;

    public Electronics(double price) {
        this.price = price;
    }

    public double getPrice() {
        return price;
    }

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

// 价格计算访问者
public class DiscountPriceVisitor implements PriceVisitor {
    private double totalPrice = 0;

    @Override
    public void visit(Book book) {
        totalPrice += book.getPrice() * 0.9; // 书籍9折
    }

    @Override
    public void visit(Electronics electronics) {
        totalPrice += electronics.getPrice() * 0.95; // 电子产品95折
    }

    public double getTotalPrice() {
        return totalPrice;
    }
}
策略模式实现
java 复制代码
// 策略接口
public interface PricingStrategy {
    double calculatePrice(double originalPrice);
}

// 书籍折扣策略
public class BookPricingStrategy implements PricingStrategy {
    @Override
    public double calculatePrice(double originalPrice) {
        return originalPrice * 0.9; // 书籍9折
    }
}

// 电子产品折扣策略
public class ElectronicsPricingStrategy implements PricingStrategy {
    @Override
    public double calculatePrice(double originalPrice) {
        return originalPrice * 0.95; // 电子产品95折
    }
}

// 商品类
public class Item {
    private double price;
    private PricingStrategy pricingStrategy;

    public Item(double price, PricingStrategy pricingStrategy) {
        this.price = price;
        this.pricingStrategy = pricingStrategy;
    }

    public double getDiscountedPrice() {
        return pricingStrategy.calculatePrice(price);
    }
}

差异分析

  • 访问者模式:适合处理多种类型的元素,操作(如折扣计算)集中于访问者,元素类无需知道具体逻辑。但新增元素类型需修改所有访问者。
  • 策略模式:适合单一职责的算法切换,商品类直接持有策略,逻辑更简单。但若元素类型增多,需为每种类型定义策略类,略显冗余。
  • 适用性:访问者模式适合复杂对象结构的多样化操作,策略模式适合单一行为的动态替换。

总结

访问者模式是一把锋利的工具,通过将操作与数据结构解耦,为复杂对象结构的处理提供了无与伦比的灵活性。它的核心价值在于支持动态扩展操作,保持元素类的纯净,特别适合报表生成、AST 解析等场景。在 Spring 中,SpEL 的表达式解析展示了访问者模式的优雅实现,让开发者能够以最小代价扩展功能。与策略模式相比,访问者模式在处理多类型元素时更具优势,但需警惕其复杂性和对元素类型稳定性的要求。实际开发中,开发者应根据对象结构的复杂度和操作变更的频率选择是否采用访问者模式。当你面对一个需要频繁添加操作的固定结构时,访问者模式将是你的得力助手,助你打造模块化、可维护的系统。

相关推荐
程序员小陈在成都18 分钟前
Spring Ioc源码引入:什么是IoC,IoC解决了什么问题
spring
bug菌1 小时前
面十年开发候选人被反问:当类被标注为@Service后,会有什么好处?我...🫨
spring boot·后端·spring
麓殇⊙1 小时前
设计模式--桥接模式详解
设计模式·桥接模式
学习机器不会机器学习2 小时前
深入浅出JavaScript常见设计模式:从原理到实战(1)
开发语言·javascript·设计模式
阿杜杜不是阿木木3 小时前
03.使用spring-ai玩转MCP
java·人工智能·spring boot·spring·mcp·spring-ai
Stimd3 小时前
【重写SpringFramework】声明式事务上:构建事务切面(chapter 4-5)
java·后端·spring
caihuayuan43 小时前
【docker&redis】用docker容器运行单机redis
java·大数据·sql·spring·课程设计
学编程的小程5 小时前
Spring MVC深度解析:从原理到实战
java·spring·mvc
zuckzhao955 小时前
Spring Security入门学习(一)Helloworld项目
java·学习·spring
ApeAssistant6 小时前
Spring + 设计模式 (二十) 行为型 - 中介者模式
spring·设计模式