Pengiriman Ganda di DDD

1. Ikhtisar

Pengiriman ganda adalah istilah teknis untuk menggambarkan proses pemilihan metode yang akan dipanggil berdasarkan tipe penerima dan argumen.

Banyak pengembang sering bingung antara pengiriman ganda dengan Pola Strategi.

Java tidak mendukung pengiriman ganda, tetapi ada beberapa teknik yang dapat kami terapkan untuk mengatasi batasan ini.

Dalam tutorial ini, kami akan fokus untuk menunjukkan contoh pengiriman ganda dalam konteks Domain-driven Design (DDD) dan Pola Strategi.

2. Pengiriman Ganda

Sebelum kita membahas pengiriman ganda, mari kita tinjau beberapa dasar dan jelaskan apa itu Pengiriman Tunggal.

2.1. Pengiriman Tunggal

Pengiriman tunggal adalah cara untuk memilih implementasi metode berdasarkan jenis runtime penerima. Di Jawa, ini pada dasarnya sama dengan polimorfisme.

Misalnya, mari kita lihat antarmuka kebijakan diskon sederhana ini:

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

The DiscountPolicy antarmuka memiliki dua implementasi. Yang datar, yang selalu mengembalikan diskon yang sama:

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

Dan implementasi kedua, yang mengembalikan diskon berdasarkan total biaya 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 kebutuhan contoh ini, anggap kelas Order memiliki metode totalCost () .

Sekarang, pengiriman tunggal di Java hanyalah perilaku polimorfik yang sangat terkenal yang ditunjukkan dalam pengujian 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); }

Jika ini semua tampak cukup mudah, pantau terus. Kami akan menggunakan contoh yang sama nanti.

Kami sekarang siap untuk memperkenalkan pengiriman ganda.

2.2. Pengiriman Ganda vs Kelebihan Metode

Pengiriman ganda menentukan metode yang akan dipanggil saat runtime berdasarkan tipe penerima dan tipe argumen .

Java tidak mendukung pengiriman ganda.

Perhatikan bahwa pengiriman ganda sering kali disalahartikan sebagai metode overloading, yang bukan merupakan hal yang sama . Metode overloading memilih metode yang akan dipanggil hanya berdasarkan informasi waktu kompilasi, seperti tipe deklarasi variabel.

Contoh berikut menjelaskan perilaku ini secara rinci.

Mari perkenalkan antarmuka diskon baru yang disebut SpecialDiscountPolicy :

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

SpecialOrder hanya memperpanjang Order tanpa menambahkan perilaku baru.

Sekarang, ketika kita membuat instance SpecialOrder tetapi mendeklarasikannya sebagai Pesanan normal , maka metode diskon khusus 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 karena itu, kelebihan metode bukanlah pengiriman ganda.

Meskipun Java tidak mendukung pengiriman ganda, kita dapat menggunakan pola untuk mencapai perilaku serupa: Pengunjung.

2.3. Pola Pengunjung

Pola Pengunjung memungkinkan kita untuk menambahkan perilaku baru ke kelas yang ada tanpa mengubahnya . Ini dimungkinkan berkat teknik cerdik dalam meniru pengiriman ganda.

Mari kita tinggalkan sejenak contoh diskon agar kita bisa memperkenalkan pola Pengunjung.

Bayangkan kita ingin menghasilkan tampilan HTML menggunakan template berbeda untuk setiap jenis pesanan . Kami dapat menambahkan perilaku ini langsung ke kelas pesanan, tetapi itu bukan ide terbaik karena merupakan pelanggaran SRP.

Sebagai gantinya, kami akan menggunakan pola Pengunjung.

Pertama, kita perlu memperkenalkan antarmuka yang Dapat Dikunjungi :

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

Kami juga akan menggunakan antarmuka pengunjung, dalam kasing kami bernama OrderVisitor :

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

Namun, salah satu kelemahan dari pola Pengunjung adalah bahwa dibutuhkan kelas yang dapat dikunjungi untuk mengetahui Pengunjung.

Jika kelas tidak dirancang untuk mendukung Pengunjung, mungkin sulit (atau bahkan tidak mungkin jika kode sumber tidak tersedia) untuk menerapkan pola ini.

Setiap jenis pesanan perlu mengimplementasikan antarmuka yang Dapat Dikunjungi dan menyediakan implementasinya sendiri yang tampaknya identik, kelemahan lainnya.

Perhatikan bahwa metode yang ditambahkan ke Order dan SpecialOrder identik:

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 tergoda untuk tidak menerapkan ulang accept di subclass. Namun, jika tidak, maka metode OrderVisitor.visit (Order) akan selalu digunakan, tentu saja, karena polimorfisme.

Terakhir, mari kita lihat implementasi OrderVisitor yang bertanggung jawab untuk membuat tampilan 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. Pengiriman Ganda di 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.