1. 概述
双分派(Double Dispatch) 是一种根据接收者(receiver)和参数类型动态决定调用哪个方法的技术术语。
很多开发者会将双分派和策略模式(Strategy Pattern)混淆。Java 并不原生支持双分派,但我们可以通过一些技巧来模拟这种行为,最常见的是使用访问者模式(Visitor Pattern)。
在本教程中,我们将通过领域驱动设计(DDD)的上下文,展示双分派的使用场景,并解释它与策略模式的区别。
2. 双分派详解
在讲解双分派之前,我们先回顾一下 Java 中的单分派(Single Dispatch)机制。
2.1. 单分派(Single Dispatch)
单分派是根据接收者(对象)的运行时类型来决定调用哪个方法。 这其实就是 Java 中多态的体现。
举个例子,我们定义一个折扣策略接口:
public interface DiscountPolicy {
double discount(Order order);
}
它有两个实现:
- 固定折扣策略:
public class FlatDiscountPolicy implements DiscountPolicy {
@Override
public double discount(Order order) {
return 0.01;
}
}
- 根据订单金额决定折扣:
public class AmountBasedDiscountPolicy implements DiscountPolicy {
@Override
public double discount(Order order) {
if (order.totalCost().isGreaterThan(Money.of(CurrencyUnit.USD, 500.00))) {
return 0.10;
} else {
return 0;
}
}
}
下面是一个测试示例,展示了单分派的行为:
@Test
void test() {
DiscountPolicy flatPolicy = new FlatDiscountPolicy();
DiscountPolicy amountPolicy = new AmountBasedDiscountPolicy();
Order orderWorth501Dollars = orderWorthNDollars(501);
double flatDiscount = flatPolicy.discount(orderWorth501Dollars);
double amountDiscount = amountPolicy.discount(orderWorth501Dollars);
assertThat(flatDiscount).isEqualTo(0.01);
assertThat(amountDiscount).isEqualTo(0.1);
}
✅ 总结:单分派只根据接收者类型决定调用哪个方法。
2.2. 双分派 vs 方法重载
双分派不仅考虑接收者类型,还考虑参数类型来决定调用哪个方法。
Java 不支持双分派,但可以通过访问者模式来模拟。
很多人误以为方法重载就是双分派,其实不是。方法重载是编译期行为,只根据变量的声明类型决定调用哪个方法。
举个例子:
public interface SpecialDiscountPolicy extends DiscountPolicy {
double discount(SpecialOrder order);
}
public class SpecialOrder extends Order { ... }
如果我们这样写:
Order specialOrder = new SpecialOrder(...);
specialPolicy.discount(specialOrder);
即使实际类型是 SpecialOrder
,调用的仍然是 discount(Order order)
方法。
❌ 这说明方法重载不是双分派!
2.3. 使用访问者模式模拟双分派
访问者模式允许我们在不修改原有类的情况下,为其添加新的行为。
举个例子:我们想为不同类型的订单生成不同的 HTML 展示。
定义接口:
public interface Visitable<V> {
void accept(V visitor);
}
public interface OrderVisitor {
void visit(Order order);
void visit(SpecialOrder order);
}
让订单类实现 Visitable<OrderVisitor>
:
public class Order implements Visitable<OrderVisitor> {
@Override
public void accept(OrderVisitor visitor) {
visitor.visit(this);
}
}
public class SpecialOrder extends Order {
@Override
public void accept(OrderVisitor visitor) {
visitor.visit(this);
}
}
定义访问者实现:
public class HtmlOrderViewCreator implements OrderVisitor {
private String html;
public String getHtml() {
return html;
}
@Override
public void visit(Order order) {
html = String.format("<p>Regular order total cost: %s</p>", order.totalCost());
}
@Override
public void visit(SpecialOrder order) {
html = String.format("<h1>Special Order</h1><p>total cost: %s</p>", order.totalCost());
}
}
使用方式:
List<Order> orders = Arrays.asList(new Order(...), new SpecialOrder(...));
HtmlOrderViewCreator visitor = new HtmlOrderViewCreator();
orders.get(0).accept(visitor);
String regularHtml = visitor.getHtml();
orders.get(1).accept(visitor);
String specialHtml = visitor.getHtml();
✅ 这样就能根据订单类型动态调用不同的方法,模拟了双分派行为。
⚠️ 缺点:需要修改订单类,添加 accept
方法。
3. DDD 中的双分派实践
在 DDD 中,我们经常需要根据订单类型应用不同的折扣策略。这时候双分派就派上用场了。
3.1. 折扣策略与策略模式
我们定义了 DiscountPolicy
接口,用于支持多种折扣策略:
public interface DiscountPolicy {
double discount(Order order);
}
✅ 这其实就是策略模式的一种体现。
3.2. 双分派与折扣策略结合
我们希望在计算订单总价时,能够根据订单类型动态选择折扣策略。
例如:
public class Order {
public Money totalCost(SpecialDiscountPolicy policy) {
return totalCost().multipliedBy(1 - applyDiscountPolicy(policy), RoundingMode.HALF_UP);
}
protected double applyDiscountPolicy(SpecialDiscountPolicy policy) {
return policy.discount(this);
}
}
子类 SpecialOrder
重写该方法:
public class SpecialOrder extends Order {
@Override
protected double applyDiscountPolicy(SpecialDiscountPolicy policy) {
return policy.discount(this);
}
}
这样我们就能根据订单类型调用不同的折扣逻辑。
举个例子:
Order order = new SpecialOrder(...);
SpecialDiscountPolicy policy = new SpecialDiscountPolicy() {
@Override
public double discount(Order order) { return 0; }
@Override
public double discount(SpecialOrder order) {
if (order.isEligibleForExtraDiscount()) return 0.20;
return 0.10;
}
};
Money cost = order.totalCost(policy);
✅ 这样就能避免类型转换和反射,优雅地实现基于订单类型的折扣计算。
4. 总结
在本文中,我们介绍了:
- ✅ 单分派是 Java 多态的体现;
- ❌ 方法重载不是双分派;
- ✅ 双分派可以通过访问者模式模拟;
- ✅ 在 DDD 中,双分派与策略模式结合可以优雅地实现复杂业务逻辑;
- ✅ 避免类型转换,提升代码可读性和可维护性。
完整示例代码可在 GitHub 获取。