Doppelversand in DDD

1. Übersicht

Double Dispatch ist ein technischer Begriff, der den Prozess der Auswahl der aufzurufenden Methode basierend auf dem Empfänger- und dem Argumenttyp beschreibt.

Viele Entwickler verwechseln häufig den doppelten Versand mit dem Strategiemuster.

Java unterstützt keinen doppelten Versand, aber es gibt Techniken, mit denen wir diese Einschränkung überwinden können.

In diesem Tutorial konzentrieren wir uns auf Beispiele für Doppelversand im Kontext von Domain-Driven Design (DDD) und Strategiemuster.

2. Doppelversand

Bevor wir uns mit Doppelversand befassen, wollen wir einige Grundlagen überprüfen und erklären, was Einzelversand eigentlich ist.

2.1. Einzelversand

Mit dem Einzelversand können Sie die Implementierung einer Methode basierend auf dem Laufzeittyp des Empfängers auswählen. In Java ist dies im Grunde dasselbe wie Polymorphismus.

Schauen wir uns zum Beispiel diese einfache Benutzeroberfläche für Rabattrichtlinien an:

public interface DiscountPolicy { double discount(Order order); }

Die DiscountPolicy- Schnittstelle verfügt über zwei Implementierungen. Die flache, die immer den gleichen Rabatt zurückgibt:

public class FlatDiscountPolicy implements DiscountPolicy { @Override public double discount(Order order) { return 0.01; } }

Und die zweite Implementierung, die den Rabatt basierend auf den Gesamtkosten der Bestellung zurückgibt:

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; } } }

Nehmen wir für die Anforderungen dieses Beispiels an, dass die Order- Klasse eine totalCost () -Methode hat.

Der Einzelversand in Java ist nur ein sehr bekanntes polymorphes Verhalten, das im folgenden Test demonstriert wurde:

@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); }

Wenn dies alles ziemlich einfach erscheint, bleiben Sie dran. Wir werden das gleiche Beispiel später verwenden.

Wir sind jetzt bereit, den doppelten Versand einzuführen.

2.2. Double Dispatch vs Method Overloading

Der doppelte Versand bestimmt die Methode, die zur Laufzeit aufgerufen werden soll, basierend auf dem Empfängertyp und den Argumenttypen .

Java unterstützt keinen doppelten Versand.

Beachten Sie, dass Doppelversand häufig mit Methodenüberladung verwechselt wird, was nicht dasselbe ist . Das Überladen von Methoden wählt die aufzurufende Methode nur basierend auf Informationen zur Kompilierungszeit aus, z. B. dem Deklarationstyp der Variablen.

Das folgende Beispiel erläutert dieses Verhalten im Detail.

Lassen Sie uns eine neue Rabattschnittstelle namens SpecialDiscountPolicy einführen :

public interface SpecialDiscountPolicy extends DiscountPolicy { double discount(SpecialOrder order); }

SpecialOrder erweitert Order einfach, ohne dass ein neues Verhalten hinzugefügt wird.

Wenn wir nun eine Instanz von SpecialOrder erstellen , diese aber als normale Bestellung deklarieren , wird die spezielle Rabattmethode nicht verwendet:

@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); }

Daher ist das Überladen von Methoden kein doppelter Versand.

Auch wenn Java keinen doppelten Versand unterstützt, können wir ein Muster verwenden, um ein ähnliches Verhalten zu erzielen: Besucher.

2.3. Besuchermuster

Das Besuchermuster ermöglicht es uns, den vorhandenen Klassen neues Verhalten hinzuzufügen, ohne sie zu ändern . Dies ist dank der cleveren Technik der Emulation des Doppelversands möglich.

Lassen wir das Rabattbeispiel für einen Moment, damit wir das Besuchermuster vorstellen können.

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.

Instead, we'll use the Visitor pattern.

First, we need to introduce the Visitable interface:

public interface Visitable { void accept(V visitor); }

We'll also use a visitor interface, in our cased named 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.

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.

Each order type needs to implement the Visitable interface and provide its own implementation which is seemingly identical, another drawback.

Notice that the added methods to Order and SpecialOrder are identical:

public class Order implements Visitable { @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.

Finally, let's see the implementation of OrderVisitor responsible for creating HTML views:

public class HtmlOrderViewCreator implements OrderVisitor { private String html; public String getHtml() { return html; } @Override public void visit(Order order) { html = String.format("

Regular order total cost: %s

", order.totalCost()); } @Override public void visit(SpecialOrder order) { html = String.format("

total cost: %s

", order.totalCost()); } }

The following example demonstrates the use of 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 anyOrderLines = OrderFixtureUtils.anyOrderLines(); List 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("

Regular order total cost: .*

"); assertThat(specialOrderHtml).containsPattern("

total cost: .*

"); }

3. Double Dispatch in DDD

In previous sections, we discussed double dispatch and the Visitor pattern.

We're now finally ready to show how to use these techniques in DDD.

Let's go back to the example of orders and discount policies.

3.1. Discount Policy as a Strategy Pattern

Earlier, we introduced the Order class and its totalCost() method that calculates the sum of all order line items:

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.

This design is much more supple than simply hardcoding all possible discount policies in Order classes:

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.

Let's see how to combine the double dispatch technique and discount policy.

3.2. Double Dispatch and Discount Policy

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:

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.

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:

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.

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.

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.

Let's override this method in SpecialOrder class:

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:

@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

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.