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();
}
}
然后在控制器中直接使用这个新的访问者即可,无需修改订单、订单明细等代码。
六、总结
本文通过电商订单的场景,展示了访问者模式的使用,主要收获如下:
- 分离数据结构和操作:订单明细只负责存储数据,所有操作都由访问者实现,符合单一职责原则;
- 符合开闭原则:新增操作只需要新增访问者类,无需修改数据结构的代码;
- 集中管理操作逻辑:同一类操作的逻辑都在对应的访问者中,便于维护和复用;
- 灵活扩展:可以方便地为不同类型的元素实现不同的操作逻辑。
访问者模式适用于数据结构稳定、操作易变的场景,比如报表生成、数据导出、规则校验等。但如果数据结构经常变化,新增或删除元素子类,会导致所有访问者都需要修改,此时不适合使用访问者模式。

浙公网安备 33010602011771号