Перейти к основному содержимому

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

· 9 мин. чтения

1. Обзор

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

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

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

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

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

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

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

Единая диспетчеризация — это способ выбрать реализацию метода на основе типа среды выполнения приемника. В Java это в основном то же самое, что и полиморфизм.

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

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

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

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 имеет метод totalCost() .

Итак, одиночная отправка в 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 не поддерживает двойную отправку.

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

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

Давайте представим новый интерфейс скидки под названием SpecialDiscountPolicy :

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

SpecialOrder просто расширяет Order без добавления нового поведения.

Теперь, когда мы создаем экземпляр SpecialOrder , но объявляем его как обычный 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<V> {
void accept(V visitor);
}

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

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

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

**

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

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

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

public class Order implements Visitable<OrderVisitor> {
@Override
public void accept(OrderVisitor visitor) {
visitor.visit(this);
}
}

public class SpecialOrder extends Order {
@Override
public void accept(OrderVisitor visitor) {
visitor.visit(this);
}
}

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

Наконец, давайте посмотрим на реализацию OrderVisitor, отвечающую за создание представлений HTML:

public class HtmlOrderViewCreator implements OrderVisitor {

private String html;

public String getHtml() {
return html;
}

@Override
public void visit(Order order) {
html = String.format("<p>Regular order total cost: %s</p>", order.totalCost());
}

@Override
public void visit(SpecialOrder order) {
html = String.format("<h1>Special Order</h1><p>total cost: %s</p>", order.totalCost());
}

}

В следующем примере демонстрируется использование 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<OrderLine> anyOrderLines = OrderFixtureUtils.anyOrderLines();
List<Order> 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("<p>Regular order total cost: .*</p>");
assertThat(specialOrderHtml).containsPattern("<h1>Special Order</h1><p>total cost: .*</p>");
}

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

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

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

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

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

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

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

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

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

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

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

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

3.2. Двойная отправка и политика скидок

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

Например, класс Order может реализовать totalCost следующим образом:

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

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

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

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

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

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

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

Благодаря такому дизайну мы избегаем дублирования бизнес-логики в методе totalCost в подклассах 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));
}

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

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

Давайте переопределим этот метод в классе SpecialOrder :

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

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

@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. Вывод

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

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