SpringBoot使用设计模式一访问者模式

一、前言

SpringBoot的业务开发中,我们偶尔会遇到这样的场景:系统中存在一组结构稳定的对象集合,但是需要对这组对象执行的操作却经常变化。比如电商系统中的订单模块,订单里包含了商品、优惠券、运费、税费等不同类型的明细对象,业务上可能需要对订单明细做金额汇总、数据导出、风控校验、发票开具等多种操作;如果把这些操作都写在明细对象的内部,会导致对象职责臃肿,新增操作时还需要修改所有明细类的代码,违背开闭原则。
比如下面的实现方式,就存在明显的问题:

// 订单明细基类
public abstract class OrderItem {
    protected String name;
    protected BigDecimal amount;

    public OrderItem(String name, BigDecimal amount) {
        this.name = name;
        this.amount = amount;
    }

    // 金额汇总
    public abstract BigDecimal calculateTotal();
    // 导出数据
    public abstract String exportData();
    // 风控校验
    public abstract boolean riskCheck();
    // 新增操作时,需要修改这个基类和所有子类
}

// 商品明细子类
public class GoodsItem extends OrderItem {
    public GoodsItem(String name, BigDecimal amount) {
        super(name, amount);
    }

    @Override
    public BigDecimal calculateTotal() {
        return this.amount;
    }

    @Override
    public String exportData() {
        return "商品:" + this.name + ",金额:" + this.amount;
    }

    @Override
    public boolean riskCheck() {
        return this.amount.compareTo(new BigDecimal("10000")) < 0;
    }
}

// 优惠券明细子类
public class CouponItem extends OrderItem {
    public CouponItem(String name, BigDecimal amount) {
        super(name, amount);
    }

    @Override
    public BigDecimal calculateTotal() {
        return this.amount.negate();
    }

    @Override
    public String exportData() {
        return "优惠券:" + this.name + ",抵扣金额:" + this.amount;
    }

    @Override
    public boolean riskCheck() {
        return this.amount.compareTo(new BigDecimal("500")) < 0;
    }
}

这种写法会让OrderItem的职责越来越杂,每次新增操作都要修改所有子类,代码的可维护性会越来越差。针对这种对象结构稳定、操作易变的场景,我们可以使用访问者模式来优化。

二、访问者模式

访问者模式(Visitor Pattern)是一种行为型设计模式,它的核心作用是将数据结构与数据操作分离,把对对象集合的操作封装为独立的访问者类,使得新增操作时无需修改数据结构的代码,只需要新增访问者即可。

核心角色

  • 抽象元素(Element):定义接受访问者的方法,是数据结构的抽象,如订单明细的基类;
  • 具体元素(Concrete Element):抽象元素的实现类,是数据结构的具体实现,如商品明细、优惠券明细;
  • 抽象访问者(Visitor):定义对所有具体元素的访问方法,每个方法对应一种操作,如金额汇总、数据导出;
  • 具体访问者(Concrete Visitor):抽象访问者的实现类,实现对不同具体元素的具体操作逻辑;
  • 对象结构(Object Structure):管理元素集合的容器,提供遍历元素的方法,如订单对象,用于存放所有订单明细。

使用场景

  • 数据结构稳定,但需要对数据执行的操作经常变化的场景;
  • 需要对一组对象执行多种不相关的操作,且不想让这些操作污染对象本身的代码;
  • 希望在不修改对象结构的前提下,为对象新增操作。

优点

  • 分离数据结构和操作,符合单一职责原则,对象只负责存储数据,操作由访问者实现;
  • 符合开闭原则,新增操作只需要新增访问者类,无需修改数据结构的代码;
  • 集中管理同一类操作的逻辑,比如所有的导出操作都在导出访问者中,便于维护和复用;
  • 可以方便地为不同类型的元素实现不同的操作逻辑。

缺点

  • 增加了系统的复杂度,引入了多个新的角色和类;
  • 依赖于具体元素的类型,若数据结构发生变化(比如新增元素子类),需要修改所有访问者的代码;
  • 破坏了元素的封装性,访问者需要直接访问元素的内部属性。

注意事项

  • 访问者模式适用于数据结构稳定的场景,如果数据结构经常变化,不适合使用;
  • 访问者可以访问元素的内部属性,使用时需要注意数据的封装性;
  • 可以结合工厂模式管理访问者,方便客户端获取对应的访问者。

三、实现案例

SpringBoot电商系统的订单明细处理为例,使用访问者模式分离订单明细的数据结构和操作逻辑,实现金额汇总、数据导出、风控校验三种操作,并且支持新增操作。

3.1 定义抽象元素与具体元素

抽象元素(OrderItem)

定义接受访问者的方法,所有具体元素都需要实现这个方法:

/**
 * 抽象订单明细(抽象元素)
 */
public abstract class OrderItem {
    protected String name;
    protected BigDecimal amount;

    public OrderItem(String name, BigDecimal amount) {
        this.name = name;
        this.amount = amount;
    }

    // 接受访问者的方法,将自身传递给访问者
    public abstract void accept(OrderVisitor visitor);

    // getter方法,供访问者访问内部属性
    public String getName() {
        return name;
    }

    public BigDecimal getAmount() {
        return amount;
    }
}

具体元素

实现抽象元素,实现accept方法,将自身传递给访问者:

/**
 * 商品明细(具体元素)
 */
public class GoodsItem extends OrderItem {
    public GoodsItem(String name, BigDecimal amount) {
        super(name, amount);
    }

    @Override
    public void accept(OrderVisitor visitor) {
        // 调用访问者的访问商品明细的方法
        visitor.visitGoodsItem(this);
    }
}

/**
 * 优惠券明细(具体元素)
 */
public class CouponItem extends OrderItem {
    public CouponItem(String name, BigDecimal amount) {
        super(name, amount);
    }

    @Override
    public void accept(OrderVisitor visitor) {
        // 调用访问者的访问优惠券明细的方法
        visitor.visitCouponItem(this);
    }
}

/**
 * 运费明细(具体元素)
 */
public class FreightItem extends OrderItem {
    public FreightItem(String name, BigDecimal amount) {
        super(name, amount);
    }

    @Override
    public void accept(OrderVisitor visitor) {
        // 调用访问者的访问运费明细的方法
        visitor.visitFreightItem(this);
    }
}

3.2 定义抽象访问者与具体访问者

抽象访问者(OrderVisitor)

定义对所有具体元素的访问方法,每个方法对应一种操作:

/**
 * 抽象订单访问者(抽象访问者)
 */
public interface OrderVisitor {
    // 访问商品明细
    void visitGoodsItem(GoodsItem goodsItem);
    // 访问优惠券明细
    void visitCouponItem(CouponItem couponItem);
    // 访问运费明细
    void visitFreightItem(FreightItem freightItem);
}

具体访问者

实现抽象访问者,实现对不同元素的具体操作逻辑:

/**
 * 金额汇总访问者(具体访问者)
 */
@Service
public class TotalAmountVisitor implements OrderVisitor {
    // 汇总结果
    private BigDecimal total = BigDecimal.ZERO;

    @Override
    public void visitGoodsItem(GoodsItem goodsItem) {
        // 商品金额直接累加
        total = total.add(goodsItem.getAmount());
    }

    @Override
    public void visitCouponItem(CouponItem couponItem) {
        // 优惠券金额是抵扣,需要减去
        total = total.subtract(couponItem.getAmount());
    }

    @Override
    public void visitFreightItem(FreightItem freightItem) {
        // 运费金额累加
        total = total.add(freightItem.getAmount());
    }

    // 获取汇总结果
    public BigDecimal getTotal() {
        return total;
    }
}

/**
 * 数据导出访问者(具体访问者)
 */
@Service
public class ExportDataVisitor implements OrderVisitor {
    // 导出结果
    private StringBuilder exportContent = new StringBuilder();

    @Override
    public void visitGoodsItem(GoodsItem goodsItem) {
        exportContent.append("商品:").append(goodsItem.getName())
                .append(",金额:").append(goodsItem.getAmount()).append("\n");
    }

    @Override
    public void visitCouponItem(CouponItem couponItem) {
        exportContent.append("优惠券:").append(couponItem.getName())
                .append(",抵扣金额:").append(couponItem.getAmount()).append("\n");
    }

    @Override
    public void visitFreightItem(FreightItem freightItem) {
        exportContent.append("运费:").append(freightItem.getName())
                .append(",金额:").append(freightItem.getAmount()).append("\n");
    }

    // 获取导出结果
    public String getExportContent() {
        return exportContent.toString();
    }
}

/**
 * 风控校验访问者(具体访问者)
 */
@Service
public class RiskCheckVisitor implements OrderVisitor {
    // 校验结果
    private boolean pass = true;

    @Override
    public void visitGoodsItem(GoodsItem goodsItem) {
        // 商品金额超过1万,风控不通过
        if (goodsItem.getAmount().compareTo(new BigDecimal("10000")) >= 0) {
            pass = false;
        }
    }

    @Override
    public void visitCouponItem(CouponItem couponItem) {
        // 优惠券抵扣超过500,风控不通过
        if (couponItem.getAmount().compareTo(new BigDecimal("500")) >= 0) {
            pass = false;
        }
    }

    @Override
    public void visitFreightItem(FreightItem freightItem) {
        // 运费超过200,风控不通过
        if (freightItem.getAmount().compareTo(new BigDecimal("200")) >= 0) {
            pass = false;
        }
    }

    // 获取校验结果
    public boolean isPass() {
        return pass;
    }
}

3.3 定义对象结构(订单)

管理元素集合,提供遍历元素的方法,供访问者遍历所有元素:

/**
 * 订单(对象结构)
 */
@Data
public class Order {
    private String orderNo;
    private List<OrderItem> itemList = new ArrayList<>();

    public Order(String orderNo) {
        this.orderNo = orderNo;
    }

    // 添加订单明细
    public void addItem(OrderItem item) {
        itemList.add(item);
    }

    // 接受访问者,遍历所有明细并让访问者访问
    public void accept(OrderVisitor visitor) {
        for (OrderItem item : itemList) {
            item.accept(visitor);
        }
    }
}

3.4 控制器层调用(客户端)

客户端通过Spring注入访问者,创建订单对象,然后让访问者访问订单中的所有明细:

@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {

    @Autowired
    private TotalAmountVisitor totalAmountVisitor;

    @Autowired
    private ExportDataVisitor exportDataVisitor;

    @Autowired
    private RiskCheckVisitor riskCheckVisitor;

    /**
     * 创建订单并执行操作
     */
    @PostMapping("/create")
    public R<?> createOrder() {
        // 创建订单
        Order order = new Order("ORDER_20250618_001");
        // 添加订单明细
        order.addItem(new GoodsItem("手机", new BigDecimal("8999")));
        order.addItem(new CouponItem("满减优惠券", new BigDecimal("300")));
        order.addItem(new FreightItem("顺丰快递", new BigDecimal("20")));

        // 1. 金额汇总
        order.accept(totalAmountVisitor);
        BigDecimal total = totalAmountVisitor.getTotal();
        log.info("订单总金额:{}", total);

        // 2. 数据导出
        order.accept(exportDataVisitor);
        String exportContent = exportDataVisitor.getExportContent();
        log.info("订单导出数据:\n{}", exportContent);

        // 3. 风控校验
        order.accept(riskCheckVisitor);
        boolean riskPass = riskCheckVisitor.isPass();
        log.info("风控校验结果:{}", riskPass ? "通过" : "不通过");

        return R.success("订单操作完成");
    }
}

四、优化访问者的管理

为了优化访问者的获取和使用逻辑,避免在控制器中直接注入多个访问者,可结合工厂模式封装访问者的管理逻辑:

4.1 定义访问者工厂


/**
 * 订单访问者工厂(管理所有访问者)
 */
@Component
@Slf4j
public class OrderVisitorFactory {

    /**
     * 缓存访问者:key=访问者类型,value=对应的访问者
     */
    private static final Map<String, OrderVisitor> VISITOR_MAP = new ConcurrentHashMap<>();

    /**
     * 注入所有OrderVisitor的实现类(Spring自动扫描)
     */
    @Resource
    private List<OrderVisitor> visitorList;

    /**
     * 初始化:将访问者注册到缓存(项目启动时执行)
     */
    @PostConstruct
    public void init() {
        for (OrderVisitor visitor : visitorList) {
            String visitorType = visitor.getClass().getSimpleName();
            VISITOR_MAP.put(visitorType, visitor);
            log.info("注册订单访问者:{} -> {}", visitorType, visitor.getClass().getName());
        }
    }

    /**
     * 根据访问者类型获取访问者
     * @param visitorType 访问者类型(类名)
     * @return 对应的访问者对象
     */
    public OrderVisitor getVisitor(String visitorType) {
        OrderVisitor visitor = VISITOR_MAP.get(visitorType);
        if (visitor == null) {
            throw new IllegalArgumentException("不支持的访问者类型:" + visitorType);
        }
        return visitor;
    }
}

4.2 改造控制器调用逻辑

@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {

    @Autowired
    private OrderVisitorFactory visitorFactory;

    /**
     * 创建订单并执行操作
     */
    @PostMapping("/create")
    public R<?> createOrder() {
        // 创建订单
        Order order = new Order("ORDER_20250618_001");
        order.addItem(new GoodsItem("手机", new BigDecimal("8999")));
        order.addItem(new CouponItem("满减优惠券", new BigDecimal("300")));
        order.addItem(new FreightItem("顺丰快递", new BigDecimal("20")));

        // 1. 金额汇总
        TotalAmountVisitor totalVisitor = (TotalAmountVisitor) visitorFactory.getVisitor("TotalAmountVisitor");
        order.accept(totalVisitor);
        log.info("订单总金额:{}", totalVisitor.getTotal());

        // 2. 数据导出
        ExportDataVisitor exportVisitor = (ExportDataVisitor) visitorFactory.getVisitor("ExportDataVisitor");
        order.accept(exportVisitor);
        log.info("订单导出数据:\n{}", exportVisitor.getExportContent());

        // 3. 风控校验
        RiskCheckVisitor riskVisitor = (RiskCheckVisitor) visitorFactory.getVisitor("RiskCheckVisitor");
        order.accept(riskVisitor);
        log.info("风控校验结果:{}", riskVisitor.isPass() ? "通过" : "不通过");

        return R.success("订单操作完成");
    }
}

五、新增操作的实现

如果需要新增一个操作,比如发票开具,只需要新增一个具体访问者即可,无需修改任何数据结构的代码:

/**
 * 发票开具访问者(新增的具体访问者)
 */
@Service
public class InvoiceGenerateVisitor implements OrderVisitor {
    // 发票内容
    private StringBuilder invoiceContent = new StringBuilder();

    @Override
    public void visitGoodsItem(GoodsItem goodsItem) {
        invoiceContent.append("商品:").append(goodsItem.getName())
                .append(",金额:").append(goodsItem.getAmount()).append("(可抵扣)\n");
    }

    @Override
    public void visitCouponItem(CouponItem couponItem) {
        invoiceContent.append("优惠券:").append(couponItem.getName())
                .append(",抵扣金额:").append(couponItem.getAmount()).append("(不可抵扣)\n");
    }

    @Override
    public void visitFreightItem(FreightItem freightItem) {
        invoiceContent.append("运费:").append(freightItem.getName())
                .append(",金额:").append(freightItem.getAmount()).append("(可抵扣)\n");
    }

    // 获取发票内容
    public String getInvoiceContent() {
        return invoiceContent.toString();
    }
}

然后在控制器中直接使用这个新的访问者即可,无需修改订单、订单明细等代码。

六、总结

本文通过电商订单的场景,展示了访问者模式的使用,主要收获如下:

  • 分离数据结构和操作:订单明细只负责存储数据,所有操作都由访问者实现,符合单一职责原则;
  • 符合开闭原则:新增操作只需要新增访问者类,无需修改数据结构的代码;
  • 集中管理操作逻辑:同一类操作的逻辑都在对应的访问者中,便于维护和复用;
  • 灵活扩展:可以方便地为不同类型的元素实现不同的操作逻辑。

访问者模式适用于数据结构稳定、操作易变的场景,比如报表生成、数据导出、规则校验等。但如果数据结构经常变化,新增或删除元素子类,会导致所有访问者都需要修改,此时不适合使用访问者模式。

posted @ 2026-01-06 20:42  夏尔_717  阅读(3)  评论(0)    收藏  举报