1. Overview
1.概述
Double dispatch is a technical term to describe the process of choosing the method to invoke based both on receiver and argument types.
双重调度是一个技术术语,用于描述根据接收器和参数类型选择要调用的方法的过程。
A lot of developers often confuse double dispatch with Strategy Pattern.
很多开发人员经常将双重调度与策略模式混淆。
Java doesn’t support double dispatch, but there are techniques we can employ to overcome this limitation.
Java不支持双重调度,但我们可以采用一些技术来克服这一限制。
In this tutorial, we’ll focus on showing examples of double dispatch in the context of Domain-driven Design (DDD) and Strategy Pattern.
在本教程中,我们将重点展示在领域驱动设计(DDD)和策略模式背景下的双重调度的例子。
2. Double Dispatch
2.双重派遣
Before we discuss double dispatch, let’s review some basics and explain what Single Dispatch actually is.
在我们讨论双重调度之前,让我们回顾一下一些基础知识,并解释一下单次调度究竟是什么。
2.1. Single Dispatch
2.1.单一调度
Single dispatch is a way to choose the implementation of a method based on the receiver runtime type. In Java, this is basically the same thing as polymorphism.
Single dispatch是一种根据接收者的运行时类型来选择方法实现的方式。在Java中,这与多态性基本相同。
For example, let’s take a look at this simple discount policy interface:
例如,让我们看一下这个简单的折扣政策界面。
public interface DiscountPolicy {
double discount(Order order);
}
The DiscountPolicy interface has two implementations. The flat one, which always returns the same discount:
DiscountPolicy接口有两种实现方式。扁平的,总是返回相同的折扣。
public class FlatDiscountPolicy implements DiscountPolicy {
@Override
public double discount(Order order) {
return 0.01;
}
}
And the second implementation, which returns the discount based on the total cost of the order:
而第二个实现,则是根据订单的总成本返回折扣。
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;
}
}
}
For the needs of this example, let’s assume the Order class has a totalCost() method.
为了这个例子的需要,我们假设Order类有一个totalCost()方法。
Now, single dispatch in Java is just a very well-known polymorphic behavior demonstrated in the following test:
现在,Java中的单次派发只是一个非常著名的多态行为,在以下测试中得到了证明:。
@DisplayName(
"given two discount policies, " +
"when use these policies, " +
"then single dispatch chooses the implementation based on runtime type"
)
@Test
void test() throws Exception {
// given
DiscountPolicy flatPolicy = new FlatDiscountPolicy();
DiscountPolicy amountPolicy = new AmountBasedDiscountPolicy();
Order orderWorth501Dollars = orderWorthNDollars(501);
// when
double flatDiscount = flatPolicy.discount(orderWorth501Dollars);
double amountDiscount = amountPolicy.discount(orderWorth501Dollars);
// then
assertThat(flatDiscount).isEqualTo(0.01);
assertThat(amountDiscount).isEqualTo(0.1);
}
If this all seems pretty straight-forward, stay tuned. We’ll use the same example later.
如果这一切看起来很简单,请继续关注。我们稍后将使用同一个例子。。
We’re now ready to introduce double dispatch.
我们现在已经准备好引入双重派遣。
2.2. Double Dispatch vs Method Overloading
2.2.双重调度与方法重载
Double dispatch determines the method to invoke at runtime based both on the receiver type and the argument types.
Double dispatch在运行时根据接收器类型和参数类型来确定要调用的方法。
Java doesn’t support double dispatch.
Java不支持双重调度。
Note that double dispatch is often confused with method overloading, which is not the same thing. Method overloading chooses the method to invoke based only on compile-time information, like the declaration type of the variable.
请注意,双重调度经常与方法重载相混淆,这不是一回事。方法重载只根据编译时的信息(如变量的声明类型)来选择调用的方法。
The following example explains this behavior in detail.
下面的例子详细解释了这种行为。
Let’s introduce a new discount interface called SpecialDiscountPolicy:
让我们来介绍一个新的折扣接口,叫做SpecialDiscountPolicy。
public interface SpecialDiscountPolicy extends DiscountPolicy {
double discount(SpecialOrder order);
}
SpecialOrder simply extends Order with no new behavior added.
SpecialOrder简单地扩展了Order,没有添加新的行为。
Now, when we create an instance of SpecialOrder but declare it as normal Order, then the special discount method is not used:
现在,当我们创建一个SpecialOrder的实例,但将其声明为普通的Order,那么就不会使用特殊折扣方法。
@DisplayName(
"given discount policy accepting special orders, " +
"when apply the policy on special order declared as regular order, " +
"then regular discount method is used"
)
@Test
void test() throws Exception {
// given
SpecialDiscountPolicy specialPolicy = new SpecialDiscountPolicy() {
@Override
public double discount(Order order) {
return 0.01;
}
@Override
public double discount(SpecialOrder order) {
return 0.10;
}
};
Order specialOrder = new SpecialOrder(anyOrderLines());
// when
double discount = specialPolicy.discount(specialOrder);
// then
assertThat(discount).isEqualTo(0.01);
}
Therefore, method overloading is not double dispatch.
因此,方法重载不是双重派遣。
Even if Java doesn’t support double dispatch, we can use a pattern to achieve similar behavior: Visitor.
即使Java不支持双重调度,我们也可以使用一个模式来实现类似的行为。Visitor。
2.3. Visitor Pattern
2.3.访客模式
The Visitor pattern allows us to add new behavior to the existing classes without modifying them. This is possible thanks to the clever technique of emulating double dispatch.
Visitor模式允许我们在不修改现有类的情况下为其添加新的行为。这要归功于模拟双重调度的巧妙技术。
Let’s leave the discount example for a moment so we can introduce the Visitor pattern.
让我们暂时抛开打折的例子,这样我们就可以介绍访客模式。
Imagine we’d like to produce HTML views using different templates for each kind of order. We could add this behavior directly to the order classes, but it’s not the best idea due to being an SRP violation.
想象一下,我们想为每种订单使用不同的模板制作HTML视图。我们可以将这种行为直接添加到订单类中,但这不是最好的主意,因为它违反了SRP。
Instead, we’ll use the Visitor pattern.
相反,我们将使用访问者模式。
First, we need to introduce the Visitable interface:
首先,我们需要介绍Visitable接口。
public interface Visitable<V> {
void accept(V visitor);
}
We’ll also use a visitor interface, in our cased named OrderVisitor:
我们还将使用一个访问者接口,在我们的例子中,名为OrderVisitor。
public interface OrderVisitor {
void visit(Order order);
void visit(SpecialOrder order);
}
However, one of the drawbacks of the Visitor pattern is that it requires visitable classes to be aware of the Visitor.
然而,Visitor模式的一个缺点是,它要求可访问的类必须知道Visitor。
If classes were not designed to support the Visitor, it might be hard (or even impossible if the source code is not available) to apply this pattern.
如果类的设计不是为了支持Visitor,可能很难(如果没有源代码,甚至不可能)应用这种模式。
Each order type needs to implement the Visitable interface and provide its own implementation which is seemingly identical, another drawback.
每个订单类型都需要实现Visitable接口,并提供自己的实现,这似乎是相同的,这是另外一个缺点。
Notice that the added methods to Order and SpecialOrder are identical:
注意,添加到Order和SpecialOrder的方法是相同的。
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);
}
}
It might be tempting to not re-implement accept in the subclass. However, if we didn’t, then the OrderVisitor.visit(Order) method would always get used, of course, due to polymorphism.
在子类中不重新实现accept可能会很诱人。然而,如果我们不这样做,那么OrderVisitor.visit(Order)方法将总是被使用,当然,由于多态性的原因。
Finally, let’s see the implementation of OrderVisitor responsible for creating HTML views:
最后,让我们看看负责创建HTML视图的OrderVisitor的实现。
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());
}
}
The following example demonstrates the use of HtmlOrderViewCreator:
下面的例子演示了HtmlOrderViewCreator的使用。
@DisplayName(
"given collection of regular and special orders, " +
"when create HTML view using visitor for each order, " +
"then the dedicated view is created for each order"
)
@Test
void test() throws Exception {
// given
List<OrderLine> anyOrderLines = OrderFixtureUtils.anyOrderLines();
List<Order> orders = Arrays.asList(new Order(anyOrderLines), new SpecialOrder(anyOrderLines));
HtmlOrderViewCreator htmlOrderViewCreator = new HtmlOrderViewCreator();
// when
orders.get(0)
.accept(htmlOrderViewCreator);
String regularOrderHtml = htmlOrderViewCreator.getHtml();
orders.get(1)
.accept(htmlOrderViewCreator);
String specialOrderHtml = htmlOrderViewCreator.getHtml();
// then
assertThat(regularOrderHtml).containsPattern("<p>Regular order total cost: .*</p>");
assertThat(specialOrderHtml).containsPattern("<h1>Special Order</h1><p>total cost: .*</p>");
}
3. Double Dispatch in DDD
3.DDD中的双重调度
In previous sections, we discussed double dispatch and the Visitor pattern.
在前面的章节中,我们讨论了双调度和Visitor模式。
We’re now finally ready to show how to use these techniques in DDD.
我们现在终于准备好展示如何在DDD中使用这些技术了。。
Let’s go back to the example of orders and discount policies.
让我们回到订单和折扣政策的例子上。
3.1. Discount Policy as a Strategy Pattern
3.1.折扣政策作为一种战略模式
Earlier, we introduced the Order class and its totalCost() method that calculates the sum of all order line items:
早些时候,我们介绍了Order类和它的totalCost()方法,该方法计算了所有订单行项目的总数。
public class Order {
public Money totalCost() {
// ...
}
}
There’s also the DiscountPolicy interface to calculate the discount for the order. This interface was introduced to allow using different discount policies and change them at runtime.
还有一个DiscountPolicy接口来计算订单的折扣。引入这个接口是为了允许使用不同的折扣政策并在运行时改变它们。
This design is much more supple than simply hardcoding all possible discount policies in Order classes:
这种设计比简单地在Order类中硬编码所有可能的折扣政策要灵活得多。
public interface DiscountPolicy {
double discount(Order order);
}
We haven’t mentioned this explicitly so far, but this example uses the Strategy pattern. DDD often uses this pattern to conform to the Ubiquitous Language principle and achieve low coupling. In the DDD world, the Strategy pattern is often named Policy.
到目前为止,我们还没有明确提到这一点,但是这个示例使用了策略模式。DDD经常使用这种模式来符合泛在语言原则,并实现低耦合。在DDD世界中,策略模式通常被命名为策略。
Let’s see how to combine the double dispatch technique and discount policy.
让我们来看看如何将双发技术和折扣政策结合起来。
3.2. Double Dispatch and Discount Policy
3.2.双重派送和折扣政策
To properly use the Policy pattern, it’s often a good idea to pass it as an argument. This approach follows the Tell, Don’t Ask principle which supports better encapsulation.
为了正确使用策略模式,将其作为参数传递往往是一个好主意。这种方法遵循告诉,不要问原则,支持更好的封装。
For example, the Order class might implement totalCost like so:
例如,Order类可以像这样实现totalCost。
public class Order /* ... */ {
// ...
public Money totalCost(SpecialDiscountPolicy discountPolicy) {
return totalCost().multipliedBy(1 - discountPolicy.discount(this), RoundingMode.HALF_UP);
}
// ...
}
Now, let’s assume we’d like to process each type of order differently.
现在,让我们假设我们想以不同的方式处理每种类型的订单。
For example, when calculating the discount for special orders, there are some other rules requiring information unique to the SpecialOrder class. We want to avoid casting and reflection and at the same time be able to calculate total costs for each Order with the correctly applied discount.
例如,在计算特殊订单的折扣时,有一些其他规则需要SpecialOrder类特有的信息。我们希望避免铸造和反射,同时能够计算出每个Order的总成本,并正确应用折扣。
We already know that method overloading happens at compile-time. So, the natural question arises: how can we dynamically dispatch order discount logic to the right method based on the runtime type of the order?
我们已经知道,方法重载发生在编译时。因此,自然会产生一个问题:我们如何根据订单的运行时类型,动态地将订单折扣逻辑分派给正确的方法?
The answer? We need to modify order classes slightly.
答案是什么?我们需要稍微修改一下订单类。
The root Order class needs to dispatch to the discount policy argument at runtime. The easiest way to achieve this is to add a protected applyDiscountPolicy method:
根Order类需要在运行时调度到折扣政策参数。实现这一目标的最简单方法是添加一个受保护的applyDiscountPolicy方法。
public class Order /* ... */ {
// ...
public Money totalCost(SpecialDiscountPolicy discountPolicy) {
return totalCost().multipliedBy(1 - applyDiscountPolicy(discountPolicy), RoundingMode.HALF_UP);
}
protected double applyDiscountPolicy(SpecialDiscountPolicy discountPolicy) {
return discountPolicy.discount(this);
}
// ...
}
Thanks to this design, we avoid duplicating business logic in the totalCost method in Order subclasses.
由于这种设计,我们避免了在Order子类的totalCost方法中重复业务逻辑。
Let’s show a demo of usage:
让我们展示一个使用演示。
@DisplayName(
"given regular order with items worth $100 total, " +
"when apply 10% discount policy, " +
"then cost after discount is $90"
)
@Test
void test() throws Exception {
// given
Order order = new Order(OrderFixtureUtils.orderLineItemsWorthNDollars(100));
SpecialDiscountPolicy discountPolicy = new SpecialDiscountPolicy() {
@Override
public double discount(Order order) {
return 0.10;
}
@Override
public double discount(SpecialOrder order) {
return 0;
}
};
// when
Money totalCostAfterDiscount = order.totalCost(discountPolicy);
// then
assertThat(totalCostAfterDiscount).isEqualTo(Money.of(CurrencyUnit.USD, 90));
}
This example still uses the Visitor pattern but in a slightly modified version. Order classes are aware that SpecialDiscountPolicy (the Visitor) has some meaning and calculates the discount.
这个例子仍然使用Visitor模式,但有一个稍微修改的版本。订单类知道SpecialDiscountPolicy(访问者)有一些意义,并计算折扣。
As mentioned previously, we want to be able to apply different discount rules based on the runtime type of Order. Therefore, we need to override the protected applyDiscountPolicy method in every child class.
如前所述,我们希望能够根据Order的运行时类型来应用不同的折扣规则。因此,我们需要在每个子类中覆盖受保护的applyDiscountPolicy方法。
Let’s override this method in SpecialOrder class:
让我们在SpecialOrder类中覆盖这个方法。
public class SpecialOrder extends Order {
// ...
@Override
protected double applyDiscountPolicy(SpecialDiscountPolicy discountPolicy) {
return discountPolicy.discount(this);
}
// ...
}
We can now use extra information about SpecialOrder in the discount policy to calculate the right discount:
我们现在可以使用折扣政策中关于SpecialOrder的额外信息来计算出正确的折扣。
@DisplayName(
"given special order eligible for extra discount with items worth $100 total, " +
"when apply 20% discount policy for extra discount orders, " +
"then cost after discount is $80"
)
@Test
void test() throws Exception {
// given
boolean eligibleForExtraDiscount = true;
Order order = new SpecialOrder(OrderFixtureUtils.orderLineItemsWorthNDollars(100),
eligibleForExtraDiscount);
SpecialDiscountPolicy discountPolicy = 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;
}
};
// when
Money totalCostAfterDiscount = order.totalCost(discountPolicy);
// then
assertThat(totalCostAfterDiscount).isEqualTo(Money.of(CurrencyUnit.USD, 80.00));
}
Additionally, since we are using polymorphic behavior in order classes, we can easily modify the total cost calculation method.
此外,由于我们在订单类中使用多态行为,我们可以很容易地修改总成本计算方法。
4. Conclusion
4.结论
In this article, we’ve learned how to use double dispatch technique and Strategy (aka Policy) pattern in Domain-driven design.
在这篇文章中,我们已经学习了如何在领域驱动设计中使用双调度技术和策略(又名政策)模式。
The full source code of all the examples is available over on GitHub.
所有示例的完整源代码都可以在GitHub上找到。