Envio duplo em DDD

Envio duplo em DDD

1. Visão geral

Despacho duplo é um termo técnico para descrever oprocess of choosing the method to invoke based both on receiver and argument types.

Muitos desenvolvedores freqüentemente confundem despacho duplo comStrategy Pattern.

Java não suporta despacho duplo, mas existem técnicas que podemos empregar para superar essa limitação.

Neste tutorial, vamos nos concentrar em mostrar exemplos de despacho duplo no contexto de Domain-driven Design (DDD) e Strategy Pattern.

2. Expedição dupla

Antes de discutirmos o despacho duplo, vamos revisar alguns princípios básicos e explicar o que realmente é o despacho único.

2.1. Despacho único

Single dispatch is a way to choose the implementation of a method based on the receiver runtime type. Em Java, isso é basicamente a mesma coisa que polimorfismo.

Por exemplo, vamos dar uma olhada nesta interface simples de política de desconto:

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

A interfaceDiscountPolicy tem duas implementações. O plano, que sempre retorna o mesmo desconto:

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

E a segunda implementação, que retorna o desconto com base no custo total do 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 as necessidades deste exemplo, vamos assumir que a classeOrder tem um métodototalCost().

Agora, o despacho único em Java é apenas um comportamento polimórfico muito conhecido demonstrado no seguinte teste:

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

Se tudo isso parecer bem direto, fique atento. We’ll use the same example later.

Agora estamos prontos para apresentar o despacho duplo.

2.2. Despacho Duplo x Sobrecarga de Método

Double dispatch determines the method to invoke at runtime based both on the receiver type and the argument types.

Java não suporta despacho duplo.

Note that double dispatch is often confused with method overloading, which is not the same thing. A sobrecarga de método escolhe o método a ser chamado com base apenas em informações em tempo de compilação, como o tipo de declaração da variável.

O exemplo a seguir explica esse comportamento em detalhes.

Vamos apresentar uma nova interface de desconto chamadaSpecialDiscountPolicy:

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

SpecialOrder simplesmente estendeOrder sem nenhum novo comportamento adicionado.

Agora, quando criamos uma instância deSpecialOrder, mas a declaramos comoOrder normal, o método de desconto especial não é usado:

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

Portanto, a sobrecarga de método não é despacho duplo.

Mesmo que o Java não ofereça suporte a despacho duplo, podemos usar um padrão para atingir um comportamento semelhante:Visitor.

2.3. Padrão do visitante

The Visitor pattern allows us to add new behavior to the existing classes without modifying them. Isso é possível graças à técnica inteligente de emulação de despacho duplo.

Vamos deixar o exemplo de desconto por um momento para que possamos introduzir o padrão de visitante.

Imagine we’d like to produce HTML views using different templates for each kind of order. Poderíamos adicionar esse comportamento diretamente às classes de pedido, mas não é a melhor ideia devido a ser uma violação SRP.

Em vez disso, usaremos o padrão Visitor.

Primeiro, precisamos apresentar a interfaceVisitable:

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

Também usaremos uma interface de visitante, em nosso caso chamadoOrderVisitor:

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

No entanto, uma das desvantagens do padrão Visitor é que ele requer que as classes visitáveis ​​estejam cientes do Visitor.

Se as classes não foram projetadas para oferecer suporte ao Visitante, pode ser difícil (ou até impossível se o código-fonte não estiver disponível) aplicar esse padrão.

Cada tipo de pedido precisa implementar a interfaceVisitable e fornecer sua própria implementação que é aparentemente idêntica, outra desvantagem.

Observe que os métodos adicionados aOrder eSpecialOrder são 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);
    }
}

Pode ser tentador não reimplementaraccept na subclasse. No entanto, se não o fizéssemos, então o métodoOrderVisitor.visit(Order) sempre seria usado, é claro, devido ao polimorfismo.

Por fim, vamos ver a implementação deOrderVisitor responsável pela criação de visualizações 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("

Special Order

total cost: %s

", order.totalCost()); } }

O exemplo a seguir demonstra o uso deHtmlOrderViewCreator:

@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("

Special Order

total cost: .*

"); }

3. Envio duplo em DDD

Nas seções anteriores, discutimos o despacho duplo e o padrão Visitor.

Agora finalmente estamos prontos para mostrar como usar essas técnicas em DDD.

Vamos voltar ao exemplo de pedidos e políticas de desconto.

3.1. Política de descontos como padrão de estratégia

Anteriormente, apresentamos a classeOrder e seu métodototalCost() que calcula a soma de todos os itens de linha do pedido:

public class Order {
    public Money totalCost() {
        // ...
    }
}

Também existe a interfaceDiscountPolicy para calcular o desconto do pedido. Essa interface foi introduzida para permitir o uso de diferentes políticas de desconto e alterá-las no tempo de execução.

Este design é muito mais flexível do que simplesmente codificar todas as políticas de desconto possíveis nas classesOrder:

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

Não mencionamos isso explicitamente até agora, mas este exemplo usaStrategy pattern. O DDD geralmente usa esse padrão para se conformar ao princípioUbiquitous Languagee obter baixo acoplamento. No mundo DDD, o padrão Strategy costuma ser denominado Política.

Vamos ver como combinar a técnica de despacho duplo e política de desconto.

3.2. Política de Desconto e Envio Duplo

To properly use the Policy pattern, it’s often a good idea to pass it as an argument. Essa abordagem segue o princípioTell, Don’t Ask, que oferece suporte a um melhor encapsulamento.

Por exemplo, a classeOrder pode implementartotalCost assim:

public class Order /* ... */ {
    // ...
    public Money totalCost(SpecialDiscountPolicy discountPolicy) {
        return totalCost().multipliedBy(1 - discountPolicy.discount(this), RoundingMode.HALF_UP);
    }
    // ...
}

Agora, vamos supor que desejamos processar cada tipo de pedido de maneira diferente.

Por exemplo, ao calcular o desconto para pedidos especiais, existem algumas outras regras que exigem informações exclusivas para a classeSpecialOrder. Queremos evitar projeções e reflexos e, ao mesmo tempo, poder calcular os custos totais de cadaOrder com o desconto aplicado corretamente.

Já sabemos que a sobrecarga de método ocorre em tempo de compilação. 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?

A resposta? Precisamos modificar ligeiramente as classes de pedidos.

A classe rootOrder precisa despachar para o argumento da política de desconto no tempo de execução. A maneira mais fácil de fazer isso é adicionar um métodoapplyDiscountPolicy protegido:

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

Graças a esse design, evitamos duplicar a lógica de negócios no métodototalCost nas subclasses deOrder.

Vamos mostrar uma demonstração de uso:

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

Este exemplo ainda usa o padrão Visitor, mas em uma versão ligeiramente modificada. As classes de pedidos estão cientes de queSpecialDiscountPolicy (o Visitante) tem algum significado e calculam o desconto.

Conforme mencionado anteriormente, queremos ser capazes de aplicar regras de desconto diferentes com base no tipo de tempo de execução deOrder. Portanto, precisamos sobrescrever o método protegidoapplyDiscountPolicy em cada classe filha.

Vamos substituir este método na classeSpecialOrder:

public class SpecialOrder extends Order {
    // ...
    @Override
    protected double applyDiscountPolicy(SpecialDiscountPolicy discountPolicy) {
        return discountPolicy.discount(this);
    }
   // ...
}

Agora podemos usar informações extras sobreSpecialOrder na política de desconto para calcular o desconto certo:

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

Além disso, como estamos usando comportamento polimórfico em classes de ordem, podemos modificar facilmente o método de cálculo do custo total.

4. Conclusão

Neste artigo, aprendemos como usar a técnica de despacho duplo e o padrãoStrategy (também conhecido comoPolicy) em design orientado a domínio.

O código-fonte completo de todos os exemplos está disponívelover on GitHub.