Рубрики
Без рубрики

Двойная отправка в DDD

Взгляните на примеры двойной отправки в контексте доменного проектирования.

Автор оригинала: Mike Wojtyna.

1. Обзор

Двойная отправка-это технический термин для описания процесса выбора метода для вызова, основанного как на типах приемника, так и на типах аргументов.

Многие разработчики часто путают двойную отправку с Стратегическим шаблоном .

Java не поддерживает двойную отправку, но есть методы, которые мы можем использовать, чтобы преодолеть это ограничение.

В этом уроке мы сосредоточимся на показе примеров двойной отправки в контексте доменного проектирования (DDD) и шаблона стратегии.

2. Двойная отправка

Прежде чем мы обсудим двойную отправку, давайте рассмотрим некоторые основы и объясним, что такое единая отправка на самом деле.

2.1. Единая отправка

Одиночная отправка-это способ выбора реализации метода на основе типа среды выполнения получателя. В Java это в основном то же самое, что и полиморфизм.

Например, давайте взглянем на этот простой интерфейс политики скидок:

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

Интерфейс Политика скидок имеет две реализации. Плоская, которая всегда возвращает одну и ту же скидку:

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

И вторая реализация, которая возвращает скидку, основанную на общей стоимости заказа:

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

Для нужд этого примера предположим, что класс Order имеет метод total Cost () .

Теперь одиночная отправка в Java-это просто очень хорошо известное полиморфное поведение, продемонстрированное в следующем тесте:

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

Если все это кажется довольно прямолинейным, следите за обновлениями. Мы будем использовать тот же пример позже.

Теперь мы готовы ввести двойную отправку.

2.2. Двойная отправка против перегрузки метода

Двойная отправка определяет метод для вызова во время выполнения на основе как типа получателя, так и типов аргументов .

Java не поддерживает двойную отправку.

Обратите внимание, что двойную отправку часто путают с перегрузкой метода, что не одно и то же . Перегрузка метода выбирает метод для вызова только на основе информации о времени компиляции, такой как тип объявления переменной.

В следующем примере подробно объясняется это поведение.

Давайте представим новый интерфейс скидок под названием Специальная политика скидок :

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

Специальный заказ просто расширяет Заказ без добавления нового поведения.

Теперь, когда мы создаем экземпляр Special Order , но объявляем его как обычный Order , метод специальной скидки не используется:

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

Таким образом, перегрузка метода не является двойной отправкой.

Даже если Java не поддерживает двойную отправку, мы можем использовать шаблон для достижения аналогичного поведения: Посетитель .

2.3. Шаблон посетителя

Шаблон посетителя позволяет нам добавлять новое поведение в существующие классы, не изменяя их . Это возможно благодаря умной технике эмуляции двойной отправки.

Давайте на мгновение оставим пример со скидкой, чтобы мы могли представить шаблон посетителя.

Представьте, что мы хотели бы создавать HTML-представления, используя различные шаблоны для каждого вида заказа . Мы могли бы добавить это поведение непосредственно в классы заказов, но это не лучшая идея из-за нарушения SRP.

Вместо этого мы будем использовать шаблон посетителя.

Во-первых, нам нужно ввести интерфейс Visitable :

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

Мы также будем использовать интерфейс посетителя, в нашем случае с именем Заказать посетителя :

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

Однако одним из недостатков шаблона посетителя является то, что он требует, чтобы посещаемые классы знали о Посетителе.

Если классы не были разработаны для поддержки Посетителя, может быть трудно (или даже невозможно, если исходный код недоступен) применить этот шаблон.

Каждый тип заказа должен реализовать интерфейс Visitable и предоставить свою собственную реализацию, которая, казалось бы, идентична, еще один недостаток.

Обратите внимание, что добавленные методы в Order и Special Order идентичны:

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

Может возникнуть соблазн не внедрять его повторно принимать в подклассе. Однако, если бы мы этого не сделали, то Заказать Визит посетителя.) метод всегда будет использоваться, конечно, из-за полиморфизма.

Наконец, давайте посмотрим на реализацию Order Visitor , ответственного за создание 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()); } }

В следующем примере показано использование Html Order View Creator :

@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. Двойная отправка в DDD

В предыдущих разделах мы обсуждали двойную отправку и шаблон посетителей.

Теперь мы, наконец, готовы показать, как использовать эти методы в DDD.

Давайте вернемся к примеру с заказами и политикой скидок.

3.1. Дисконтная политика как Модель стратегии

Ранее мы ввели класс Order и его метод total Cost () , который вычисляет сумму всех позиций строки заказа:

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

Существует также интерфейс Политика скидок для расчета скидки для заказа. Этот интерфейс был введен, чтобы позволить использовать различные политики скидок и изменять их во время выполнения.

Этот дизайн гораздо более гибкий, чем просто жесткое кодирование всех возможных политик скидок в классах Order :

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

До сих пор мы не упоминали об этом явно, но в этом примере используется Шаблон стратегии . DDD часто использует этот шаблон, чтобы соответствовать Вездесущий язык принцип и достигните низкого сцепления. В мире DDD шаблон стратегии часто называют Политикой.

Давайте посмотрим, как совместить метод двойной отправки и политику скидок.

3.2. Политика Двойной отправки и Скидок

Чтобы правильно использовать шаблон политики, часто рекомендуется передать его в качестве аргумента . Этот подход следует принципу Скажи, не спрашивай , который поддерживает лучшую инкапсуляцию.

Например, класс Order может реализовать total Cost примерно так:

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

Теперь давайте предположим, что мы хотели бы обрабатывать каждый тип заказа по-разному.

Например, при расчете скидки для специальных заказов существуют некоторые другие правила, требующие информации, уникальной для класса SpecialOrder . Мы хотим избежать литья и отражения и в то же время иметь возможность рассчитать общие затраты для каждого Заказа с правильно примененной скидкой.

Мы уже знаем, что перегрузка метода происходит во время компиляции. Таким образом, возникает естественный вопрос: как мы можем динамически отправлять логику скидки заказа в правильный метод, основанный на типе времени выполнения заказа?

Ответ? Нам нужно немного изменить классы заказов.

Корневой класс Order должен отправляться в аргумент политики скидок во время выполнения. Самый простой способ добиться этого-добавить защищенный применить политику скидок метод:

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

Благодаря такой конструкции мы избегаем дублирования бизнес-логики в методе total Cost в подклассах Order .

Давайте покажем демонстрацию использования:

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

В этом примере по-прежнему используется шаблон посетителя, но в слегка измененной версии. Классы заказов знают, что Специальная политика скидок (Посетитель) имеет некоторое значение и рассчитывает скидку.

Как упоминалось ранее, мы хотим иметь возможность применять различные правила скидок в зависимости от типа среды выполнения Заказ . Поэтому нам нужно переопределить защищенный применить Политику Скидок метод в каждом дочернем классе.

Давайте переопределим этот метод в Специальном порядке классе:

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

Теперь мы можем использовать дополнительную информацию о Специальном заказе в политике скидок для расчета правильной скидки:

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

Кроме того, поскольку мы используем полиморфное поведение в классах заказов, мы можем легко изменить метод расчета общей стоимости.

4. Заключение

В этой статье мы узнали, как использовать метод двойной отправки и шаблон Strategy (он же Policy ) в доменном дизайне.

Полный исходный код всех примеров доступен на GitHub .