Penghantaran Berganda dalam DDD

1. Gambaran keseluruhan

Penghantaran berganda adalah istilah teknikal untuk menerangkan proses memilih kaedah untuk digunakan berdasarkan jenis penerima dan argumen.

Sebilangan besar pemaju sering mengelirukan pengiriman dua kali dengan Corak Strategi.

Java tidak menyokong pengiriman dua kali, tetapi ada teknik yang dapat kita gunakan untuk mengatasi batasan ini.

Dalam tutorial ini, kita akan memfokuskan pada menunjukkan contoh penghantaran dua kali dalam konteks Reka Bentuk Bergerak Domain (DDD) dan Pola Strategi.

2. Penghantaran Berganda

Sebelum kita membincangkan penghantaran dua kali, mari tinjau beberapa asas dan terangkan apa itu Single Dispatch sebenarnya.

2.1. Penghantaran Tunggal

Penghantaran tunggal adalah cara untuk memilih pelaksanaan kaedah berdasarkan jenis waktu proses penerima. Di Jawa, ini pada dasarnya sama dengan polimorfisme.

Sebagai contoh, mari kita lihat antara muka dasar diskaun mudah ini:

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

Antara muka DiscountPolicy mempunyai dua pelaksanaan. Yang rata, yang selalu memberikan potongan yang sama:

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

Dan pelaksanaan kedua, yang mengembalikan potongan berdasarkan jumlah kos pesanan:

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

Untuk keperluan contoh ini, mari kita anggap kelas Order mempunyai kaedah totalCost () .

Sekarang, penghantaran tunggal di Jawa hanyalah tingkah laku polimorfik yang sangat terkenal yang ditunjukkan dalam ujian berikut:

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

Sekiranya ini kelihatan agak lurus, nantikan. Kami akan menggunakan contoh yang sama kemudian.

Kami kini bersedia untuk memperkenalkan penghantaran dua kali.

2.2. Penghantaran Berganda vs Kaedah Overloading

Penghantaran berkembar menentukan kaedah untuk digunakan pada waktu berjalan berdasarkan jenis penerima dan jenis argumen .

Java tidak menyokong penghantaran dua kali.

Perhatikan bahawa penghantaran dua kali sering dikelirukan dengan kaedah overloading, yang bukan perkara yang sama . Overloading kaedah memilih kaedah untuk dipanggil hanya berdasarkan maklumat masa kompilasi, seperti jenis deklarasi pemboleh ubah.

Contoh berikut menerangkan tingkah laku ini secara terperinci.

Mari perkenalkan antara muka diskaun baru yang dipanggil SpecialDiscountPolicy :

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

SpecialOrder hanya memperluas Pesanan tanpa penambahan tingkah laku baru.

Sekarang, apabila kita membuat contoh SpecialOrder tetapi menyatakannya sebagai Pesanan biasa , maka kaedah diskaun khas tidak digunakan:

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

Oleh itu, kaedah overloading bukanlah penghantaran dua kali.

Walaupun Java tidak menyokong pengiriman dua kali, kita dapat menggunakan corak untuk mencapai tingkah laku yang serupa: Pelawat.

2.3. Corak Pelawat

Corak Pelawat membolehkan kita menambahkan tingkah laku baru ke kelas yang ada tanpa mengubahnya . Ini mungkin berkat teknik pandai meniru penghantaran dua kali.

Mari tinggalkan contoh diskaun sebentar agar kami dapat memperkenalkan corak Pengunjung.

Bayangkan kita ingin menghasilkan paparan HTML menggunakan templat yang berbeza untuk setiap jenis pesanan . Kami boleh menambahkan tingkah laku ini terus ke kelas pesanan, tetapi itu bukan idea terbaik kerana melanggar SRP.

Sebaliknya, kami akan menggunakan corak Pelawat.

Pertama, kita perlu memperkenalkan antara muka Visitable :

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

Kami juga akan menggunakan antara muka pelawat, dalam kotak pesanan kami bernama OrderVisitor :

public interface OrderVisitor { void visit(Order order); void visit(SpecialOrder order); }

Namun, salah satu kelemahan corak Pelawat adalah bahawa ia memerlukan kelas yang boleh dikunjungi untuk mengetahui tentang Pelawat.

Sekiranya kelas tidak dirancang untuk menyokong Pelawat, mungkin sukar (atau bahkan mustahil jika kod sumber tidak tersedia) untuk menerapkan corak ini.

Setiap jenis pesanan perlu melaksanakan antara muka Visitable dan menyediakan pelaksanaannya sendiri yang nampaknya serupa, satu lagi kelemahan.

Perhatikan bahawa kaedah tambahan untuk Order dan SpecialOrder adalah serupa:

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

Mungkin menggoda untuk tidak menerapkan kembali penerimaan di subkelas. Namun, jika tidak, kaedah OrderVisitor.visit (Order) akan selalu digunakan, tentu saja, kerana polimorfisme.

Akhirnya, mari kita lihat pelaksanaan OrderVisitor yang bertanggungjawab untuk membuat paparan HTML:

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

Contoh berikut menunjukkan penggunaan 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. Penghantaran Berganda dalam 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.