Despacho doble en DDD

1. Información general

El envío doble es un término técnico para describir el proceso de elección del método para invocar en función de los tipos de receptor y de argumento.

Muchos desarrolladores a menudo confunden el envío doble con el patrón de estrategia.

Java no admite el envío doble, pero existen técnicas que podemos emplear para superar esta limitación.

En este tutorial, nos centraremos en mostrar ejemplos de envío doble en el contexto del diseño impulsado por dominio (DDD) y el patrón de estrategia.

2. Envío doble

Antes de discutir el envío doble, repasemos algunos conceptos básicos y expliquemos qué es realmente el envío único.

2.1. Envío único

El envío único es una forma de elegir la implementación de un método según el tipo de tiempo de ejecución del receptor. En Java, esto es básicamente lo mismo que polimorfismo.

Por ejemplo, echemos un vistazo a esta sencilla interfaz de política de descuentos:

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

La interfaz DiscountPolicy tiene dos implementaciones. El plano, que siempre devuelve el mismo descuento:

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

Y la segunda implementación, que devuelve el descuento en función del coste total del pedido:

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

Para las necesidades de este ejemplo, supongamos que la clase Order tiene un método totalCost () .

Ahora, el envío único en Java es solo un comportamiento polimórfico muy conocido demostrado en la siguiente prueba:

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

Si todo esto parece bastante sencillo, estad atentos. Usaremos el mismo ejemplo más adelante.

Ahora estamos listos para introducir el envío doble.

2.2. Despacho doble frente a sobrecarga de métodos

El envío doble determina el método a invocar en tiempo de ejecución según el tipo de receptor y los tipos de argumento .

Java no admite el envío doble.

Tenga en cuenta que el envío doble a menudo se confunde con la sobrecarga de métodos, que no es lo mismo . La sobrecarga de métodos elige el método a invocar basándose solo en la información en tiempo de compilación, como el tipo de declaración de la variable.

El siguiente ejemplo explica este comportamiento en detalle.

Presentamos una nueva interfaz de descuento llamada SpecialDiscountPolicy :

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

SpecialOrder simplemente extiende Order sin agregar nuevos comportamientos.

Ahora, cuando creamos una instancia de SpecialOrder pero la declaramos como Order normal , entonces no se usa el método de descuento especial:

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

Por lo tanto, la sobrecarga de métodos no es un envío doble.

Incluso si Java no admite el envío doble, podemos usar un patrón para lograr un comportamiento similar: Visitor.

2.3. Patrón de visitante

El patrón Visitor nos permite agregar un nuevo comportamiento a las clases existentes sin modificarlas . Esto es posible gracias a la ingeniosa técnica de emular el doble despacho.

Dejemos el ejemplo de descuento por un momento para que podamos introducir el patrón Visitante.

Imagine que nos gustaría producir vistas HTML usando diferentes plantillas para cada tipo de pedido . Podríamos agregar este comportamiento directamente a las clases de pedidos, pero no es la mejor idea debido a que es una violación de SRP.

En su lugar, usaremos el patrón Visitante.

Primero, necesitamos introducir la interfaz Visitable :

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

También usaremos una interfaz de visitante, en nuestro caso llamado OrderVisitor :

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

Sin embargo, uno de los inconvenientes del patrón de Visitante es que requiere que las clases visitables conozcan al Visitante.

Si las clases no se diseñaron para admitir al visitante, podría ser difícil (o incluso imposible si el código fuente no está disponible) aplicar este patrón.

Cada tipo de orden necesita implementar la interfaz Visitable y proporcionar su propia implementación que es aparentemente idéntica, otro inconveniente.

Tenga en cuenta que los métodos agregados para Order y SpecialOrder son idénticos:

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

Puede resultar tentador no volver a implementar accept en la subclase. Sin embargo, si no lo hiciéramos , entonces el método OrderVisitor.visit (Order) siempre se usaría, por supuesto, debido al polimorfismo.

Finalmente, veamos la implementación de OrderVisitor responsable de crear vistas 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()); } }

El siguiente ejemplo demuestra el uso de 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. Despacho doble en 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.