欢迎你来读这篇博客,这篇博客主要是关于访问者模式。
其中包括访问者模式的核心思想、适用场景、双分派、优缺点、与策略模式/观察者模式/组合模式/迭代器模式的区别,以及 Java 后端开发中订单费用结构访问器的完整案例。
序言
访问者模式是 23 种设计模式里比较“劝退”的一个。
很多人第一次看到它,会觉得:
明明一个方法能解决的事,为什么要绕一圈 accept(visitor) 和 visit(element)?
这个反应很正常。
访问者模式确实不适合简单业务。
它真正适合的场景是:
对象结构比较稳定,但需要在这些对象结构上不断增加新的操作。
比如:
- AST 语法树;
- 文件目录树;
- 组织架构树;
- 报表元素结构;
- 规则表达式树;
- 订单费用明细结构;
- 工作流节点结构;
- 复杂表单组件树。
这些结构里的元素类型通常比较固定。
例如订单费用明细可能有:
- 商品明细;
- 优惠券抵扣;
- 运费;
- 税费;
- 积分抵扣。
但针对这些元素的操作可能越来越多:
- 计算总金额;
- 打印明细;
- 风控检查;
- 生成报表;
- 导出 JSON;
- 生成审计日志;
- 生成财务凭证;
- 生成页面展示 VO。
如果把这些操作都写进元素类里,元素类会越来越胖。
访问者模式的想法是:
不把所有操作塞进元素类,而是把操作封装成访问者对象,让访问者去访问这些元素并执行对应逻辑。
简单说:
访问者模式就是把“对一组对象的操作”从对象本身拆出去。
元素负责保存结构。
访问者负责执行操作。
这就像博物馆里的展品很稳定,但参观的人不同:
- 游客看热闹;
- 专家看年代;
- 保安看安全;
- 摄影师看光线;
- 馆长看价值。
展品没变,访问者不同,得到的结果不同。
正文
chapter 1:什么是访问者模式
访问者模式,英文是 Visitor Pattern,属于行为型设计模式。
它的定义是:
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
这句话有两个关键点:
- 对象结构中的元素比较稳定;
- 需要在不修改元素类的情况下新增操作。
访问者模式一般包含几个角色:
- Visitor 抽象访问者:声明访问不同元素的方法。
- ConcreteVisitor 具体访问者:实现对不同元素的具体操作。
- Element 抽象元素:声明
accept(Visitor visitor) 方法。
- ConcreteElement 具体元素:实现
accept(),通常调用 visitor.visit(this)。
- ObjectStructure 对象结构:维护一组元素,并允许访问者访问这些元素。
结构如下:
1 2 3 4 5 6 7 8 9
| ObjectStructure ├── ElementA.accept(visitor) ├── ElementB.accept(visitor) └── ElementC.accept(visitor)
Visitor ├── visit(ElementA) ├── visit(ElementB) └── visit(ElementC)
|
访问者模式最典型的代码结构是:
1 2 3
| public interface Element { void accept(Visitor visitor); }
|
1 2 3 4 5 6 7
| public class ConcreteElementA implements Element {
@Override public void accept(Visitor visitor) { visitor.visit(this); } }
|
1 2 3 4 5 6
| public interface Visitor {
void visit(ConcreteElementA element);
void visit(ConcreteElementB element); }
|
chapter 2:访问者模式解决什么问题
访问者模式解决的是:
对象结构稳定,但对这些对象的操作经常变化的问题。
假设系统中有一个订单费用结构。
元素类型包括:
现在需要做金额计算。
最直接的方式是在每个元素里写:
后来又要打印明细。
再加:
后来又要风控检查。
再加:
后来又要导出财务报表。
再加:
结果元素类越来越胖。
每个元素类里都塞满了各种操作。
访问者模式的做法是:
1 2 3 4
| AmountCalculateVisitor DetailPrintVisitor RiskCheckVisitor FinanceExportVisitor
|
每一种操作对应一个访问者。
元素类只需要提供:
新增操作时新增访问者,不修改元素类。
chapter 3:访问者模式的核心思想
访问者模式的核心思想是:
把数据结构和作用于数据结构上的操作分离。
元素类负责数据结构:
1 2 3 4
| ProductFeeItem CouponDiscountItem ShippingFeeItem TaxFeeItem
|
访问者类负责操作:
1 2 3
| AmountCalculateVisitor DetailPrintVisitor RiskCheckVisitor
|
元素不再关心所有操作。
访问者根据元素类型执行不同逻辑。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class AmountCalculateVisitor implements FeeItemVisitor {
@Override public void visit(ProductFeeItem item) { }
@Override public void visit(CouponDiscountItem item) { }
@Override public void visit(ShippingFeeItem item) { } }
|
这就是访问者模式的价值:
新增操作,不改元素;新增元素,可能要改所有访问者。
所以它适合元素类型稳定、操作频繁增加的场景。
chapter 4:什么是双分派
访问者模式里经常会提到一个概念:双分派,Double Dispatch。
这也是访问者模式看起来绕的原因。
先看普通方法调用。
1
| element.accept(visitor);
|
第一次分派发生在:
运行时根据 element 的真实类型,调用对应元素类的 accept() 方法。
例如:
1
| ProductFeeItem.accept(visitor)
|
然后在 accept() 内部:
第二次分派发生在:
这里的 this 是具体元素类型。
于是最终调用:
1
| visitor.visit(ProductFeeItem item)
|
这就是双分派:
- 根据元素真实类型调用对应
accept();
- 在
accept() 中根据具体元素类型调用访问者的 visit() 方法。
双分派解决的问题是:
在运行时正确地把不同元素类型分发给访问者对应的处理方法。
简单说:
元素告诉访问者:“我是具体的 ProductFeeItem,你按 ProductFeeItem 的方式处理我。”
这就是访问者模式最核心的技术点。
chapter 5:不用访问者模式会怎样
假设订单费用元素有一个统一接口:
1 2 3 4
| public interface FeeItem {
String name(); }
|
然后有多个实现类:
1 2 3 4
| ProductFeeItem CouponDiscountItem ShippingFeeItem TaxFeeItem
|
如果不用访问者模式,做金额计算时可能会写:
1 2 3 4 5 6 7 8 9 10 11
| for (FeeItem item : items) { if (item instanceof ProductFeeItem product) { } else if (item instanceof CouponDiscountItem coupon) { } else if (item instanceof ShippingFeeItem shipping) { } else if (item instanceof TaxFeeItem tax) { } }
|
后来要打印明细,又写一遍:
1 2 3 4 5 6 7
| for (FeeItem item : items) { if (item instanceof ProductFeeItem product) { } else if (item instanceof CouponDiscountItem coupon) { } }
|
后来要风控检查,再写一遍。
这些 instanceof 会到处散落。
访问者模式可以把这些类型判断集中到访问者结构里。
元素自己负责把具体类型交给访问者。
客户端只需要:
chapter 6:案例背景:订单费用结构访问器
下面用一个 Java 后端常见案例来讲访问者模式:订单费用结构。
假设一个订单的最终金额由多种费用项组成:
- 商品费用;
- 优惠券抵扣;
- 运费;
- 税费;
- 积分抵扣。
我们希望对这些费用项执行多种操作:
- 计算最终应付金额;
- 打印费用明细;
- 执行风控检查;
- 生成财务摘要。
如果把这些操作都写进费用项类里,费用项类会非常臃肿。
所以我们用访问者模式。
费用项是元素。
金额计算、明细打印、风控检查是访问者。
chapter 7:定义抽象元素 FeeItem
1 2 3 4 5 6
| public interface FeeItem {
String name();
void accept(FeeItemVisitor visitor); }
|
每个费用项都要支持:
这是访问者模式的关键入口。
chapter 8:定义访问者接口 FeeItemVisitor
1 2 3 4 5 6 7 8 9 10 11 12
| public interface FeeItemVisitor {
void visit(ProductFeeItem item);
void visit(CouponDiscountItem item);
void visit(ShippingFeeItem item);
void visit(TaxFeeItem item);
void visit(PointDiscountItem item); }
|
访问者接口中为每种具体元素类型定义一个 visit() 方法。
这看起来有点繁琐,但它的好处是:
访问者可以针对每种元素类型写不同逻辑。
chapter 9:定义商品费用元素 ProductFeeItem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| import java.math.BigDecimal;
public class ProductFeeItem implements FeeItem {
private final Long productId;
private final String productName;
private final BigDecimal unitPrice;
private final int quantity;
public ProductFeeItem(Long productId, String productName, BigDecimal unitPrice, int quantity) { this.productId = productId; this.productName = productName; this.unitPrice = unitPrice; this.quantity = quantity; }
@Override public String name() { return "商品费用"; }
@Override public void accept(FeeItemVisitor visitor) { visitor.visit(this); }
public Long getProductId() { return productId; }
public String getProductName() { return productName; }
public BigDecimal getUnitPrice() { return unitPrice; }
public int getQuantity() { return quantity; }
public BigDecimal subtotal() { return unitPrice.multiply(BigDecimal.valueOf(quantity)); } }
|
商品费用是正向金额。
它有单价和数量。
chapter 10:定义优惠券抵扣元素 CouponDiscountItem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import java.math.BigDecimal;
public class CouponDiscountItem implements FeeItem {
private final Long couponId;
private final String couponName;
private final BigDecimal discountAmount;
public CouponDiscountItem(Long couponId, String couponName, BigDecimal discountAmount) { this.couponId = couponId; this.couponName = couponName; this.discountAmount = discountAmount; }
@Override public String name() { return "优惠券抵扣"; }
@Override public void accept(FeeItemVisitor visitor) { visitor.visit(this); }
public Long getCouponId() { return couponId; }
public String getCouponName() { return couponName; }
public BigDecimal getDiscountAmount() { return discountAmount; } }
|
优惠券是负向金额。
计算总额时要减掉它。
chapter 11:定义运费元素 ShippingFeeItem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import java.math.BigDecimal;
public class ShippingFeeItem implements FeeItem {
private final String shippingType;
private final BigDecimal shippingAmount;
public ShippingFeeItem(String shippingType, BigDecimal shippingAmount) { this.shippingType = shippingType; this.shippingAmount = shippingAmount; }
@Override public String name() { return "运费"; }
@Override public void accept(FeeItemVisitor visitor) { visitor.visit(this); }
public String getShippingType() { return shippingType; }
public BigDecimal getShippingAmount() { return shippingAmount; } }
|
chapter 12:定义税费元素 TaxFeeItem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import java.math.BigDecimal;
public class TaxFeeItem implements FeeItem {
private final String taxType;
private final BigDecimal taxAmount;
public TaxFeeItem(String taxType, BigDecimal taxAmount) { this.taxType = taxType; this.taxAmount = taxAmount; }
@Override public String name() { return "税费"; }
@Override public void accept(FeeItemVisitor visitor) { visitor.visit(this); }
public String getTaxType() { return taxType; }
public BigDecimal getTaxAmount() { return taxAmount; } }
|
chapter 13:定义积分抵扣元素 PointDiscountItem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import java.math.BigDecimal;
public class PointDiscountItem implements FeeItem {
private final int usedPoints;
private final BigDecimal discountAmount;
public PointDiscountItem(int usedPoints, BigDecimal discountAmount) { this.usedPoints = usedPoints; this.discountAmount = discountAmount; }
@Override public String name() { return "积分抵扣"; }
@Override public void accept(FeeItemVisitor visitor) { visitor.visit(this); }
public int getUsedPoints() { return usedPoints; }
public BigDecimal getDiscountAmount() { return discountAmount; } }
|
积分抵扣也是负向金额。
chapter 14:定义对象结构 OrderFeeStructure
对象结构负责维护一组元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import java.util.ArrayList; import java.util.List;
public class OrderFeeStructure {
private final List<FeeItem> feeItems = new ArrayList<>();
public void add(FeeItem feeItem) { feeItems.add(feeItem); }
public void accept(FeeItemVisitor visitor) { for (FeeItem feeItem : feeItems) { feeItem.accept(visitor); } }
public List<FeeItem> getFeeItems() { return List.copyOf(feeItems); } }
|
OrderFeeStructure 不是必须角色,但在访问者模式中很常见。
它负责让访问者遍历整个对象结构。
chapter 15:定义金额计算访问者 AmountCalculateVisitor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import java.math.BigDecimal;
public class AmountCalculateVisitor implements FeeItemVisitor {
private BigDecimal totalAmount = BigDecimal.ZERO;
@Override public void visit(ProductFeeItem item) { totalAmount = totalAmount.add(item.subtotal()); }
@Override public void visit(CouponDiscountItem item) { totalAmount = totalAmount.subtract(item.getDiscountAmount()); }
@Override public void visit(ShippingFeeItem item) { totalAmount = totalAmount.add(item.getShippingAmount()); }
@Override public void visit(TaxFeeItem item) { totalAmount = totalAmount.add(item.getTaxAmount()); }
@Override public void visit(PointDiscountItem item) { totalAmount = totalAmount.subtract(item.getDiscountAmount()); }
public BigDecimal getTotalAmount() { if (totalAmount.compareTo(BigDecimal.ZERO) < 0) { return BigDecimal.ZERO; }
return totalAmount; } }
|
这个访问者只负责金额计算。
不同元素对金额的影响不同:
- 商品费用:加;
- 运费:加;
- 税费:加;
- 优惠券:减;
- 积分抵扣:减。
这些逻辑集中在金额计算访问者中。
chapter 16:定义费用明细打印访问者 DetailPrintVisitor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| public class DetailPrintVisitor implements FeeItemVisitor {
private final StringBuilder builder = new StringBuilder();
@Override public void visit(ProductFeeItem item) { builder.append("商品:") .append(item.getProductName()) .append(",单价:") .append(item.getUnitPrice()) .append(",数量:") .append(item.getQuantity()) .append(",小计:") .append(item.subtotal()) .append("\n"); }
@Override public void visit(CouponDiscountItem item) { builder.append("优惠券:") .append(item.getCouponName()) .append(",抵扣:-") .append(item.getDiscountAmount()) .append("\n"); }
@Override public void visit(ShippingFeeItem item) { builder.append("运费类型:") .append(item.getShippingType()) .append(",金额:") .append(item.getShippingAmount()) .append("\n"); }
@Override public void visit(TaxFeeItem item) { builder.append("税费类型:") .append(item.getTaxType()) .append(",金额:") .append(item.getTaxAmount()) .append("\n"); }
@Override public void visit(PointDiscountItem item) { builder.append("积分抵扣:") .append(item.getUsedPoints()) .append(" 积分,抵扣:-") .append(item.getDiscountAmount()) .append("\n"); }
public String getDetail() { return builder.toString(); } }
|
这个访问者只负责生成明细文本。
金额计算逻辑没有污染它。
元素类也不需要知道如何打印不同格式的账单。
chapter 17:定义风控检查访问者 RiskCheckVisitor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| import java.math.BigDecimal; import java.util.ArrayList; import java.util.List;
public class RiskCheckVisitor implements FeeItemVisitor {
private final List<String> warnings = new ArrayList<>();
@Override public void visit(ProductFeeItem item) { if (item.getQuantity() <= 0) { warnings.add("商品数量异常,productId = " + item.getProductId()); }
if (item.getUnitPrice().compareTo(BigDecimal.ZERO) <= 0) { warnings.add("商品单价异常,productId = " + item.getProductId()); } }
@Override public void visit(CouponDiscountItem item) { if (item.getDiscountAmount().compareTo(new BigDecimal("500")) > 0) { warnings.add("优惠券抵扣金额过大,couponId = " + item.getCouponId()); } }
@Override public void visit(ShippingFeeItem item) { if (item.getShippingAmount().compareTo(new BigDecimal("100")) > 0) { warnings.add("运费金额过高,shippingType = " + item.getShippingType()); } }
@Override public void visit(TaxFeeItem item) { if (item.getTaxAmount().compareTo(BigDecimal.ZERO) < 0) { warnings.add("税费不能为负,taxType = " + item.getTaxType()); } }
@Override public void visit(PointDiscountItem item) { if (item.getUsedPoints() < 0) { warnings.add("积分使用数量不能为负"); } }
public boolean passed() { return warnings.isEmpty(); }
public List<String> getWarnings() { return List.copyOf(warnings); } }
|
这个访问者只负责风控检查。
它可以检查各种元素是否存在异常。
chapter 18:客户端使用访问者模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| import java.math.BigDecimal;
public class VisitorPatternDemo {
public static void main(String[] args) { OrderFeeStructure feeStructure = new OrderFeeStructure();
feeStructure.add(new ProductFeeItem( 1001L, "设计模式课程", new BigDecimal("199.00"), 1 ));
feeStructure.add(new CouponDiscountItem( 2001L, "新人优惠券", new BigDecimal("30.00") ));
feeStructure.add(new ShippingFeeItem( "STANDARD", new BigDecimal("10.00") ));
feeStructure.add(new TaxFeeItem( "VAT", new BigDecimal("5.00") ));
feeStructure.add(new PointDiscountItem( 100, new BigDecimal("10.00") ));
AmountCalculateVisitor amountVisitor = new AmountCalculateVisitor(); feeStructure.accept(amountVisitor);
System.out.println("最终应付金额:" + amountVisitor.getTotalAmount());
DetailPrintVisitor detailVisitor = new DetailPrintVisitor(); feeStructure.accept(detailVisitor);
System.out.println("费用明细:"); System.out.println(detailVisitor.getDetail());
RiskCheckVisitor riskVisitor = new RiskCheckVisitor(); feeStructure.accept(riskVisitor);
System.out.println("风控是否通过:" + riskVisitor.passed()); System.out.println("风控提示:" + riskVisitor.getWarnings()); } }
|
输出类似:
1 2 3 4 5 6 7 8 9 10
| 最终应付金额:174.00 费用明细: 商品:设计模式课程,单价:199.00,数量:1,小计:199.00 优惠券:新人优惠券,抵扣:-30.00 运费类型:STANDARD,金额:10.00 税费类型:VAT,金额:5.00 积分抵扣:100 积分,抵扣:-10.00
风控是否通过:true 风控提示:[]
|
这个例子体现了访问者模式的核心:
费用项结构没有变化,但我们可以不断增加新的访问者来实现新的操作。
chapter 19:新增操作时访问者模式的优势
假设现在要新增一个“财务摘要导出”功能。
如果没有访问者模式,可能要去每个费用项类里加方法:
使用访问者模式,只需要新增一个访问者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public class FinanceSummaryVisitor implements FeeItemVisitor {
private int positiveItemCount;
private int negativeItemCount;
private BigDecimal positiveAmount = BigDecimal.ZERO;
private BigDecimal negativeAmount = BigDecimal.ZERO;
@Override public void visit(ProductFeeItem item) { positiveItemCount++; positiveAmount = positiveAmount.add(item.subtotal()); }
@Override public void visit(CouponDiscountItem item) { negativeItemCount++; negativeAmount = negativeAmount.add(item.getDiscountAmount()); }
@Override public void visit(ShippingFeeItem item) { positiveItemCount++; positiveAmount = positiveAmount.add(item.getShippingAmount()); }
@Override public void visit(TaxFeeItem item) { positiveItemCount++; positiveAmount = positiveAmount.add(item.getTaxAmount()); }
@Override public void visit(PointDiscountItem item) { negativeItemCount++; negativeAmount = negativeAmount.add(item.getDiscountAmount()); }
public String summary() { return "正向费用项数量:" + positiveItemCount + ",正向金额:" + positiveAmount + ",抵扣项数量:" + negativeItemCount + ",抵扣金额:" + negativeAmount; } }
|
使用:
1 2 3 4 5
| FinanceSummaryVisitor visitor = new FinanceSummaryVisitor();
feeStructure.accept(visitor);
System.out.println(visitor.summary());
|
新增操作时,不需要修改任何费用项类。
这就是访问者模式最强的地方。
chapter 20:新增元素时访问者模式的痛点
访问者模式也有明显缺点。
如果现在新增一个元素类型:
那么你要做几件事:
- 新增
PackagingFeeItem;
- 在
FeeItemVisitor 接口中新增:
1
| void visit(PackagingFeeItem item);
|
- 所有具体访问者都要实现这个方法:
1 2 3 4
| AmountCalculateVisitor DetailPrintVisitor RiskCheckVisitor FinanceSummaryVisitor
|
这就是访问者模式的代价。
所以访问者模式适合:
元素类型稳定,操作经常变化。
如果你的元素类型经常变化,就不适合访问者模式。
这是访问者模式最重要的适用边界。
chapter 21:访问者模式和组合模式的关系
访问者模式经常和组合模式一起出现。
比如文件系统:
1 2 3 4
| Directory ├── File └── Directory └── File
|
组合模式负责表达树形结构。
访问者模式负责对树中不同节点执行操作。
例如:
- 统计文件大小;
- 打印目录结构;
- 搜索文件;
- 生成索引;
- 扫描敏感文件。
结构大概是:
1 2 3 4
| public interface FileSystemNode {
void accept(FileSystemVisitor visitor); }
|
1 2 3 4 5 6
| public interface FileSystemVisitor {
void visit(FileNode file);
void visit(DirectoryNode directory); }
|
组合模式解决:
对象怎么组成树?
访问者模式解决:
如何对树中不同节点增加新操作?
两者经常搭配。
chapter 22:访问者模式和迭代器模式的关系
迭代器模式负责遍历元素。
访问者模式负责对元素执行操作。
例如:
1 2 3 4 5
| Iterator<FeeItem> iterator = feeStructure.iterator();
while (iterator.hasNext()) { iterator.next().accept(visitor); }
|
区别如下:
| 对比项 |
迭代器模式 |
访问者模式 |
| 关注点 |
怎么遍历元素 |
对不同元素执行什么操作 |
| 核心方法 |
hasNext、next |
visit |
| 是否关心元素类型 |
一般不关心 |
关心具体元素类型 |
| 示例 |
遍历订单费用项 |
计算费用项总额 |
迭代器回答:
访问者回答:
chapter 23:访问者模式和策略模式的区别
访问者模式和策略模式都可以把操作拆出去,但意图不同。
| 对比项 |
访问者模式 |
策略模式 |
| 核心目的 |
给稳定对象结构增加新操作 |
替换一组算法 |
| 操作对象 |
一组不同类型元素 |
一个上下文或输入 |
| 是否依赖元素类型 |
强依赖 |
不一定 |
| 扩展方向 |
新增访问者容易 |
新增策略容易 |
| 示例 |
对费用项结构计算/打印/风控 |
VIP 折扣/新人折扣 |
策略模式通常是:
1
| strategy.calculate(context);
|
访问者模式通常是:
1
| element.accept(visitor);
|
策略模式是选择一种算法。
访问者模式是在一组对象结构上执行一种操作。
chapter 24:访问者模式和观察者模式的区别
两者名字都带“者”,但完全不是一类问题。
| 对比项 |
访问者模式 |
观察者模式 |
| 核心目的 |
对对象结构中的元素执行操作 |
状态变化后通知订阅者 |
| 触发方式 |
主动访问元素 |
被观察者发布通知 |
| 典型方法 |
accept、visit |
update、onEvent |
| 示例 |
访问订单费用项计算金额 |
订单创建后通知库存和消息模块 |
访问者是:
观察者是:
chapter 25:访问者模式和命令模式的区别
| 对比项 |
访问者模式 |
命令模式 |
| 核心目的 |
给对象结构增加操作 |
把请求封装成对象 |
| 核心对象 |
Visitor |
Command |
| 是否依赖元素结构 |
是 |
不一定 |
| 是否适合队列/撤销 |
不是重点 |
是重点 |
| 示例 |
费用项访问器 |
创建订单命令 |
命令模式关心“要做什么请求”。
访问者模式关心“对这些元素做什么操作”。
chapter 26:访问者模式和解释器模式的关系
解释器模式中经常会用访问者模式。
比如表达式树:
1 2 3
| AND ├── amount >= 100 └── userLevel == VIP
|
你可能需要对这棵树做多种操作:
- 解释执行;
- 生成 SQL;
- 打印表达式;
- 做语法检查;
- 做优化。
这些操作都可以用访问者模式实现。
例如:
1 2 3 4
| ExpressionVisitor visit(AndExpression) visit(EqualExpression) visit(GreaterThanExpression)
|
解释器模式负责定义表达式结构。
访问者模式负责给表达式结构增加操作。
chapter 27:访问者模式和面向对象封装的冲突
访问者模式有一个争议:
它看起来好像破坏了封装。
因为访问者需要知道每种具体元素的类型和数据。
例如:
1
| visit(ProductFeeItem item)
|
访问者会调用:
1 2
| item.getUnitPrice() item.getQuantity()
|
这意味着访问者依赖元素的公开接口。
所以使用访问者模式时,要注意:
- 元素需要暴露足够信息给访问者;
- 不要暴露不该暴露的内部细节;
- 访问者适合操作稳定结构,而不是随便操作任何对象。
访问者模式是一种有取舍的设计。
它用增加访问者的便利,换取新增元素时的成本。
chapter 28:Spring Boot 中如何落地访问者模式
在 Spring Boot 中,访问者通常不一定直接注册成 Bean。
如果访问者是无状态的,可以注册成 Bean。
例如:
1 2 3 4 5
| import org.springframework.stereotype.Component;
@Component public class AmountCalculateVisitor implements FeeItemVisitor { }
|
但要注意:
如果访问者内部有累积状态,比如 totalAmount,就不能直接做单例 Bean 复用。
例如:
1
| private BigDecimal totalAmount = BigDecimal.ZERO;
|
如果这个访问者是 Spring 单例,多线程下会出问题。
这种访问者应该每次请求新建。
例如:
1
| AmountCalculateVisitor visitor = new AmountCalculateVisitor();
|
或者提供工厂:
1 2 3 4 5 6 7 8 9 10 11
| @Component public class FeeItemVisitorFactory {
public AmountCalculateVisitor amountCalculateVisitor() { return new AmountCalculateVisitor(); }
public DetailPrintVisitor detailPrintVisitor() { return new DetailPrintVisitor(); } }
|
如果访问者本身无状态,比如只做校验并返回结果对象,也可以考虑设计为无状态 Bean。
chapter 29:访问者状态管理问题
访问者有两种设计方式。
1. 有状态访问者
例如:
1 2 3 4
| public class AmountCalculateVisitor {
private BigDecimal totalAmount; }
|
访问过程中累加结果。
优点是使用方便。
缺点是:
- 不能复用同一个实例;
- 多线程不安全;
- Spring 单例要谨慎。
2. 无状态访问者
也可以让访问者不保存状态,而是把结果放到外部上下文中。
1 2 3 4 5 6 7 8
| public class AmountCalculateContext {
private BigDecimal totalAmount = BigDecimal.ZERO;
public void add(BigDecimal amount) { totalAmount = totalAmount.add(amount); } }
|
访问者:
1 2 3 4 5 6 7 8
| public class AmountCalculateVisitor implements FeeItemVisitor {
private final AmountCalculateContext context;
public AmountCalculateVisitor(AmountCalculateContext context) { this.context = context; } }
|
实际项目中,要根据并发和可读性权衡。
简单场景下,有状态访问者每次 new 一个就够了。
chapter 30:访问者模式的优点
1. 新增操作容易
新增一个操作时,只需要新增访问者类。
例如:
1 2 3 4
| AmountCalculateVisitor DetailPrintVisitor RiskCheckVisitor FinanceExportVisitor
|
2. 操作逻辑集中
同一个操作针对不同元素的逻辑集中在一个访问者里。
3. 元素类更干净
元素类不需要塞入大量不属于自己的操作。
4. 适合复杂对象结构
特别适合 AST、树结构、组合结构。
5. 符合开闭原则的一部分
对新增操作开放,对元素类修改较少。
chapter 31:访问者模式的缺点
1. 新增元素类型困难
新增一个元素类型,需要修改访问者接口和所有具体访问者。
这是最大缺点。
2. 访问者依赖具体元素类型
访问者要知道具体元素类,耦合较强。
3. 类数量增加
每个操作一个访问者,访问者类会变多。
4. 理解成本较高
accept(visitor) 和 visitor.visit(this) 对初学者不直观。
5. 可能破坏封装
元素需要暴露足够数据给访问者。
chapter 32:适用场景
访问者模式适合以下场景。
1. 对象结构稳定
元素类型不经常变化。
例如 AST 节点类型、费用项类型、文件节点类型。
2. 操作经常变化
经常新增:
- 打印;
- 导出;
- 校验;
- 统计;
- 转换;
- 生成报告。
3. 需要对不同类型元素执行不同逻辑
访问者可以为每种元素提供不同 visit() 方法。
4. 不希望把操作塞进元素类
元素类保持数据结构职责。
操作类独立扩展。
5. 复杂树结构或组合结构
访问者特别适合 AST 和组合模式对象结构。
chapter 33:不适合使用的场景
以下场景不适合使用访问者模式。
1. 元素类型经常变化
如果经常新增元素类型,访问者模式会很痛苦。
2. 操作很少
如果只有一两个操作,直接写方法即可。
3. 对象结构很简单
简单 DTO 没必要访问者模式。
4. 元素不愿意暴露内部数据
访问者需要通过元素接口获取数据。
如果这会破坏封装,就要谨慎。
5. 团队不熟悉双分派
访问者模式理解成本不低。
如果团队维护困难,不要硬上。
chapter 34:真实项目中的实践建议
1. 先判断元素是否稳定
访问者模式第一问:
元素类型会不会经常新增?
如果会,经慎用。
2. 访问者命名要表达操作
推荐:
1 2 3 4 5
| AmountCalculateVisitor DetailPrintVisitor RiskCheckVisitor FinanceExportVisitor SqlGenerateVisitor
|
不要写:
1 2
| VisitorImpl1 VisitorImpl2
|
名字就是维护者的地图。
3. 注意访问者实例是否有状态
有状态访问者不要做 Spring 单例。
4. 元素的 accept 方法不要写复杂逻辑
通常只写:
不要把业务逻辑塞进 accept()。
5. 不要滥用访问者
访问者模式适合复杂对象结构,不适合普通 CRUD。
6. 可以和组合模式搭配
树形结构 + 多种操作,是访问者模式非常经典的组合。
7. 可以和解释器模式搭配
表达式树 + SQL 生成 / 解释执行 / 打印表达式,非常适合访问者。
8. 访问者接口变化影响很大
新增元素会影响所有访问者。
所以访问者接口要谨慎设计。
9. 元素数据暴露要克制
只暴露访问者真正需要的信息,不要为了访问者把内部结构全暴露。
10. 复杂访问结果可以用上下文对象
不要在访问者里堆太多字段。
可以定义:
保存中间状态。
chapter 35:完整案例代码汇总
抽象元素
1 2 3 4 5 6
| public interface FeeItem {
String name();
void accept(FeeItemVisitor visitor); }
|
访问者接口
1 2 3 4 5 6 7 8 9 10 11 12
| public interface FeeItemVisitor {
void visit(ProductFeeItem item);
void visit(CouponDiscountItem item);
void visit(ShippingFeeItem item);
void visit(TaxFeeItem item);
void visit(PointDiscountItem item); }
|
商品费用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| import java.math.BigDecimal;
public class ProductFeeItem implements FeeItem {
private final Long productId;
private final String productName;
private final BigDecimal unitPrice;
private final int quantity;
public ProductFeeItem(Long productId, String productName, BigDecimal unitPrice, int quantity) { this.productId = productId; this.productName = productName; this.unitPrice = unitPrice; this.quantity = quantity; }
@Override public String name() { return "商品费用"; }
@Override public void accept(FeeItemVisitor visitor) { visitor.visit(this); }
public Long getProductId() { return productId; }
public String getProductName() { return productName; }
public BigDecimal getUnitPrice() { return unitPrice; }
public int getQuantity() { return quantity; }
public BigDecimal subtotal() { return unitPrice.multiply(BigDecimal.valueOf(quantity)); } }
|
优惠券抵扣
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import java.math.BigDecimal;
public class CouponDiscountItem implements FeeItem {
private final Long couponId;
private final String couponName;
private final BigDecimal discountAmount;
public CouponDiscountItem(Long couponId, String couponName, BigDecimal discountAmount) { this.couponId = couponId; this.couponName = couponName; this.discountAmount = discountAmount; }
@Override public String name() { return "优惠券抵扣"; }
@Override public void accept(FeeItemVisitor visitor) { visitor.visit(this); }
public Long getCouponId() { return couponId; }
public String getCouponName() { return couponName; }
public BigDecimal getDiscountAmount() { return discountAmount; } }
|
运费
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import java.math.BigDecimal;
public class ShippingFeeItem implements FeeItem {
private final String shippingType;
private final BigDecimal shippingAmount;
public ShippingFeeItem(String shippingType, BigDecimal shippingAmount) { this.shippingType = shippingType; this.shippingAmount = shippingAmount; }
@Override public String name() { return "运费"; }
@Override public void accept(FeeItemVisitor visitor) { visitor.visit(this); }
public String getShippingType() { return shippingType; }
public BigDecimal getShippingAmount() { return shippingAmount; } }
|
税费
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import java.math.BigDecimal;
public class TaxFeeItem implements FeeItem {
private final String taxType;
private final BigDecimal taxAmount;
public TaxFeeItem(String taxType, BigDecimal taxAmount) { this.taxType = taxType; this.taxAmount = taxAmount; }
@Override public String name() { return "税费"; }
@Override public void accept(FeeItemVisitor visitor) { visitor.visit(this); }
public String getTaxType() { return taxType; }
public BigDecimal getTaxAmount() { return taxAmount; } }
|
积分抵扣
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import java.math.BigDecimal;
public class PointDiscountItem implements FeeItem {
private final int usedPoints;
private final BigDecimal discountAmount;
public PointDiscountItem(int usedPoints, BigDecimal discountAmount) { this.usedPoints = usedPoints; this.discountAmount = discountAmount; }
@Override public String name() { return "积分抵扣"; }
@Override public void accept(FeeItemVisitor visitor) { visitor.visit(this); }
public int getUsedPoints() { return usedPoints; }
public BigDecimal getDiscountAmount() { return discountAmount; } }
|
对象结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import java.util.ArrayList; import java.util.List;
public class OrderFeeStructure {
private final List<FeeItem> feeItems = new ArrayList<>();
public void add(FeeItem feeItem) { feeItems.add(feeItem); }
public void accept(FeeItemVisitor visitor) { for (FeeItem feeItem : feeItems) { feeItem.accept(visitor); } }
public List<FeeItem> getFeeItems() { return List.copyOf(feeItems); } }
|
金额计算访问者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import java.math.BigDecimal;
public class AmountCalculateVisitor implements FeeItemVisitor {
private BigDecimal totalAmount = BigDecimal.ZERO;
@Override public void visit(ProductFeeItem item) { totalAmount = totalAmount.add(item.subtotal()); }
@Override public void visit(CouponDiscountItem item) { totalAmount = totalAmount.subtract(item.getDiscountAmount()); }
@Override public void visit(ShippingFeeItem item) { totalAmount = totalAmount.add(item.getShippingAmount()); }
@Override public void visit(TaxFeeItem item) { totalAmount = totalAmount.add(item.getTaxAmount()); }
@Override public void visit(PointDiscountItem item) { totalAmount = totalAmount.subtract(item.getDiscountAmount()); }
public BigDecimal getTotalAmount() { if (totalAmount.compareTo(BigDecimal.ZERO) < 0) { return BigDecimal.ZERO; }
return totalAmount; } }
|
明细打印访问者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| public class DetailPrintVisitor implements FeeItemVisitor {
private final StringBuilder builder = new StringBuilder();
@Override public void visit(ProductFeeItem item) { builder.append("商品:") .append(item.getProductName()) .append(",单价:") .append(item.getUnitPrice()) .append(",数量:") .append(item.getQuantity()) .append(",小计:") .append(item.subtotal()) .append("\n"); }
@Override public void visit(CouponDiscountItem item) { builder.append("优惠券:") .append(item.getCouponName()) .append(",抵扣:-") .append(item.getDiscountAmount()) .append("\n"); }
@Override public void visit(ShippingFeeItem item) { builder.append("运费类型:") .append(item.getShippingType()) .append(",金额:") .append(item.getShippingAmount()) .append("\n"); }
@Override public void visit(TaxFeeItem item) { builder.append("税费类型:") .append(item.getTaxType()) .append(",金额:") .append(item.getTaxAmount()) .append("\n"); }
@Override public void visit(PointDiscountItem item) { builder.append("积分抵扣:") .append(item.getUsedPoints()) .append(" 积分,抵扣:-") .append(item.getDiscountAmount()) .append("\n"); }
public String getDetail() { return builder.toString(); } }
|
风控检查访问者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| import java.math.BigDecimal; import java.util.ArrayList; import java.util.List;
public class RiskCheckVisitor implements FeeItemVisitor {
private final List<String> warnings = new ArrayList<>();
@Override public void visit(ProductFeeItem item) { if (item.getQuantity() <= 0) { warnings.add("商品数量异常,productId = " + item.getProductId()); }
if (item.getUnitPrice().compareTo(BigDecimal.ZERO) <= 0) { warnings.add("商品单价异常,productId = " + item.getProductId()); } }
@Override public void visit(CouponDiscountItem item) { if (item.getDiscountAmount().compareTo(new BigDecimal("500")) > 0) { warnings.add("优惠券抵扣金额过大,couponId = " + item.getCouponId()); } }
@Override public void visit(ShippingFeeItem item) { if (item.getShippingAmount().compareTo(new BigDecimal("100")) > 0) { warnings.add("运费金额过高,shippingType = " + item.getShippingType()); } }
@Override public void visit(TaxFeeItem item) { if (item.getTaxAmount().compareTo(BigDecimal.ZERO) < 0) { warnings.add("税费不能为负,taxType = " + item.getTaxType()); } }
@Override public void visit(PointDiscountItem item) { if (item.getUsedPoints() < 0) { warnings.add("积分使用数量不能为负"); } }
public boolean passed() { return warnings.isEmpty(); }
public List<String> getWarnings() { return List.copyOf(warnings); } }
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| import java.math.BigDecimal;
public class VisitorPatternDemo {
public static void main(String[] args) { OrderFeeStructure feeStructure = new OrderFeeStructure();
feeStructure.add(new ProductFeeItem( 1001L, "设计模式课程", new BigDecimal("199.00"), 1 ));
feeStructure.add(new CouponDiscountItem( 2001L, "新人优惠券", new BigDecimal("30.00") ));
feeStructure.add(new ShippingFeeItem( "STANDARD", new BigDecimal("10.00") ));
feeStructure.add(new TaxFeeItem( "VAT", new BigDecimal("5.00") ));
feeStructure.add(new PointDiscountItem( 100, new BigDecimal("10.00") ));
AmountCalculateVisitor amountVisitor = new AmountCalculateVisitor(); feeStructure.accept(amountVisitor);
System.out.println("最终应付金额:" + amountVisitor.getTotalAmount());
DetailPrintVisitor detailVisitor = new DetailPrintVisitor(); feeStructure.accept(detailVisitor);
System.out.println("费用明细:"); System.out.println(detailVisitor.getDetail());
RiskCheckVisitor riskVisitor = new RiskCheckVisitor(); feeStructure.accept(riskVisitor);
System.out.println("风控是否通过:" + riskVisitor.passed()); System.out.println("风控提示:" + riskVisitor.getWarnings()); } }
|
chapter 36:一句话总结
访问者模式的本质是:
把作用于一组稳定对象结构上的操作封装成访问者,从而在不修改元素类的前提下新增操作。
它特别适合:
- AST 语法树;
- 文件系统树;
- 组织架构树;
- 订单费用结构;
- 报表元素结构;
- 规则表达式树;
- 组合模式对象结构;
- 元素稳定但操作经常增加的场景。
访问者模式最关键的判断是:
元素类型是否稳定,操作是否经常变化。
如果是,访问者模式很合适。
如果元素类型经常新增,访问者模式会让你每新增一个元素就改一堆访问者,体验会很“热闹”。
好的访问者模式像专业检查员:同一批对象,财务来算钱,风控来看风险,报表来导数据,各干各的。
坏的访问者模式像强行会诊:明明一个普通对象,非要叫十个 Visitor 排队访问,代码还没复杂,心先累了。
所以使用访问者模式时,要记住一句话:
访问者模式适合稳定结构上的多种操作,不适合普通业务对象的日常 CRUD。
参考资料
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software.
- Robert C. Martin. Agile Software Development, Principles, Patterns, and Practices.
- Martin Fowler. Refactoring: Improving the Design of Existing Code.
- Terence Parr. The Definitive ANTLR 4 Reference.
- Refactoring Guru: Visitor Pattern.
- SourceMaking: Visitor Design Pattern.
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。