1. Обзор
Доменное проектирование (DDD)-это набор принципов и инструментов, которые помогают нам разрабатывать эффективные архитектуры программного обеспечения для повышения ценности бизнеса . Ограниченный контекст является одним из центральных и существенных паттернов для спасения архитектуры от Большого Кома Грязи путем разделения всей области приложения на несколько семантически непротиворечивых частей.
В то же время с помощью модульной системы Java 9 мы можем создавать сильно инкапсулированные модули.
В этом уроке мы создадим простое приложение хранилища и посмотрим, как использовать модули Java 9 при определении явных границ для ограниченных контекстов.
2. Ограниченные контексты DDD
В настоящее время программные системы не являются простыми приложениями CRUD . На самом деле типичная монолитная корпоративная система состоит из некоторой устаревшей кодовой базы и недавно добавленных функций. Однако с каждым внесенным изменением поддерживать такие системы становится все труднее и труднее. В конце концов, это может стать совершенно недостижимым.
2.1. Ограниченный контекст и вездесущий язык
Для решения рассматриваемой проблемы DDD предоставляет концепцию ограниченного контекста. Ограниченный контекст – это логическая граница области, в которой последовательно применяются определенные термины и правила . Внутри этой границы все термины, определения и понятия из Вездесущего языка.
В частности, основное преимущество вездесущего языка заключается в объединении участников проекта из разных областей вокруг определенной бизнес-области.
Кроме того, несколько контекстов могут работать с одним и тем же. Однако в каждом из этих контекстов оно может иметь разные значения.
2.2. Контекст заказа
Давайте начнем реализацию нашего приложения с определения контекста заказа. Этот контекст содержит две сущности: Элемент заказа и Заказ клиента .
То Заказ клиента сущность-это агрегатный корень :
public class CustomerOrder {
private int orderId;
private String paymentMethod;
private String address;
private List orderItems;
public float calculateTotalPrice() {
return orderItems.stream().map(OrderItem::getTotalPrice)
.reduce(0F, Float::sum);
}
} Как мы видим, этот класс содержит метод calculate Total Price business. Но в реальном проекте это, вероятно, будет намного сложнее-например, включить скидки и налоги в окончательную цену.
Далее, давайте создадим класс OrderItem :
public class OrderItem {
private int productId;
private int quantity;
private float unitPrice;
private float unitWeight;
}Мы определили сущности, но также нам нужно предоставить некоторые API для других частей приложения. Давайте создадим класс Служба заказов клиентов :
public class CustomerOrderService implements OrderService {
public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent";
private CustomerOrderRepository orderRepository;
private EventBus eventBus;
@Override
public void placeOrder(CustomerOrder order) {
this.orderRepository.saveCustomerOrder(order);
Map payload = new HashMap<>();
payload.put("order_id", String.valueOf(order.getOrderId()));
ApplicationEvent event = new ApplicationEvent(payload) {
@Override
public String getType() {
return EVENT_ORDER_READY_FOR_SHIPMENT;
}
};
this.eventBus.publish(event);
}
} Здесь мы должны подчеркнуть некоторые важные моменты. Метод PlaceOrder отвечает за обработку заказов клиентов. После обработки заказа событие публикуется в EventBus . Мы обсудим коммуникацию, основанную на событиях, в следующих главах. Эта служба предоставляет реализацию по умолчанию для интерфейса OrderService :
public interface OrderService extends ApplicationService {
void placeOrder(CustomerOrder order);
void setOrderRepository(CustomerOrderRepository orderRepository);
}Кроме того, эта служба требует, чтобы Хранилище заказов клиентов сохраняло заказы:
public interface CustomerOrderRepository {
void saveCustomerOrder(CustomerOrder order);
}Важно то, что этот интерфейс не реализован в этом контексте, но будет предоставлен модулем инфраструктуры, как мы увидим позже.
2.3. Контекст доставки
Теперь давайте определим Контекст доставки. Он также будет простым и будет содержать три объекта: Посылка , Посылка и Отгружаемый заказ .
Давайте начнем с Отгружаемого заказа сущности:
public class ShippableOrder {
private int orderId;
private String address;
private List packageItems;
} В этом случае сущность не содержит поля Способ оплаты . Это потому, что в нашем контексте доставки нам все равно, какой способ оплаты используется. Контекст доставки отвечает только за обработку отгрузок заказов.
Кроме того, объект Parcel специфичен для контекста доставки:
public class Parcel {
private int orderId;
private String address;
private String trackingId;
private List packageItems;
public float calculateTotalWeight() {
return packageItems.stream().map(PackageItem::getWeight)
.reduce(0F, Float::sum);
}
public boolean isTaxable() {
return calculateEstimatedValue() > 100;
}
public float calculateEstimatedValue() {
return packageItems.stream().map(PackageItem::getWeight)
.reduce(0F, Float::sum);
}
} Как мы видим, он также содержит конкретные бизнес-методы и действует как совокупный корень.
Наконец, давайте определим Службу доставки посылок :
public class ParcelShippingService implements ShippingService {
public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent";
private ShippingOrderRepository orderRepository;
private EventBus eventBus;
private Map shippedParcels = new HashMap<>();
@Override
public void shipOrder(int orderId) {
Optional order = this.orderRepository.findShippableOrder(orderId);
order.ifPresent(completedOrder -> {
Parcel parcel = new Parcel(completedOrder.getOrderId(), completedOrder.getAddress(),
completedOrder.getPackageItems());
if (parcel.isTaxable()) {
// Calculate additional taxes
}
// Ship parcel
this.shippedParcels.put(completedOrder.getOrderId(), parcel);
});
}
@Override
public void listenToOrderEvents() {
this.eventBus.subscribe(EVENT_ORDER_READY_FOR_SHIPMENT, new EventSubscriber() {
@Override
public void onEvent(E event) {
shipOrder(Integer.parseInt(event.getPayloadValue("order_id")));
}
});
}
@Override
public Optional getParcelByOrderId(int orderId) {
return Optional.ofNullable(this.shippedParcels.get(orderId));
}
} Эта служба аналогичным образом использует репозиторий Заказов на доставку для получения заказов по идентификатору. Что еще более важно, он подписывается на событие Заказ Готов к отправке событие, которое публикуется другим контекстом. Когда происходит это событие, сервис применяет некоторые правила и отправляет заказ. Для простоты мы храним отгруженные заказы в HashMap .
3. Контекстные карты
До сих пор мы определяли два контекста. Однако мы не устанавливали никаких явных отношений между ними. Для этой цели DDD имеет концепцию контекстного отображения. Контекстная карта-это визуальное описание отношений между различными контекстами системы . Эта карта показывает, как различные части сосуществуют вместе, образуя домен.
Существует пять основных типов отношений между ограниченными контекстами:
- Партнерство – отношения между двумя контекстами, которые сотрудничают для согласования двух команд с зависимыми целями
- Общее ядро – своего рода связь, когда общие части нескольких контекстов извлекаются в другой контекст/модуль для уменьшения дублирования кода
- Клиент-поставщик – связь между двумя контекстами, где один контекст (восходящий) производит данные, а другой (нисходящий) потребляет их. В этих отношениях обе стороны заинтересованы в установлении наилучшего возможного общения
- Конформист – это отношение также имеет восходящий и нисходящий потоки, однако нисходящий поток всегда соответствует API восходящего потока
- Антикоррупционный уровень – этот тип отношений широко используется для устаревших систем для адаптации их к новой архитектуре и постепенной миграции из устаревшей кодовой базы. Уровень защиты от коррупции действует как адаптер для перевода данных из вышестоящего потока и защиты от нежелательных изменений
В нашем конкретном примере мы будем использовать отношение общего ядра. Мы не будем определять его в чистом виде, но он будет в основном выступать в качестве посредника событий в системе.
Таким образом, общий модуль ядра не будет содержать никаких конкретных реализаций, только интерфейсы.
Давайте начнем с интерфейса EventBus :
public interface EventBus {
void publish(E event);
void subscribe(String eventType, EventSubscriber subscriber);
void unsubscribe(String eventType, EventSubscriber subscriber);
} Этот интерфейс будет реализован позже в нашем инфраструктурном модуле.
Затем мы создаем базовый интерфейс службы с методами по умолчанию для поддержки связи, управляемой событиями:
public interface ApplicationService {
default void publishEvent(E event) {
EventBus eventBus = getEventBus();
if (eventBus != null) {
eventBus.publish(event);
}
}
default void subscribe(String eventType, EventSubscriber subscriber) {
EventBus eventBus = getEventBus();
if (eventBus != null) {
eventBus.subscribe(eventType, subscriber);
}
}
default void unsubscribe(String eventType, EventSubscriber subscriber) {
EventBus eventBus = getEventBus();
if (eventBus != null) {
eventBus.unsubscribe(eventType, subscriber);
}
}
EventBus getEventBus();
void setEventBus(EventBus eventBus);
} Таким образом, интерфейсы служб в ограниченных контекстах расширяют этот интерфейс, чтобы иметь общую функциональность, связанную с событиями.
4. Модульность Java 9
Теперь пришло время изучить, как модульная система Java 9 может поддерживать определенную структуру приложения.
Система модулей платформы Java (JPMS) поощряет создание более надежных и сильно инкапсулированных модулей. В результате эти функции могут помочь изолировать наши контексты и установить четкие границы.
Давайте посмотрим нашу окончательную схему модуля:
4.1. Общий модуль Ядра
Давайте начнем с общего модуля ядра, который не имеет никаких зависимостей от других модулей. Итак, module-info.java похоже на:
module com.baeldung.dddmodules.sharedkernel {
exports com.baeldung.dddmodules.sharedkernel.events;
exports com.baeldung.dddmodules.sharedkernel.service;
}Мы экспортируем интерфейсы модулей, чтобы они были доступны для других модулей.
4.2. Модуль Контекста заказа
Затем давайте перенесем наше внимание на модуль Контекста заказа. Для этого требуются только интерфейсы, определенные в модуле общего ядра:
module com.baeldung.dddmodules.ordercontext {
requires com.baeldung.dddmodules.sharedkernel;
exports com.baeldung.dddmodules.ordercontext.service;
exports com.baeldung.dddmodules.ordercontext.model;
exports com.baeldung.dddmodules.ordercontext.repository;
provides com.baeldung.dddmodules.ordercontext.service.OrderService
with com.baeldung.dddmodules.ordercontext.service.CustomerOrderService;
}Кроме того, мы видим, что этот модуль экспортирует реализацию по умолчанию для интерфейса OrderService .
4.3. Модуль Контекста доставки
Аналогично предыдущему модулю, давайте создадим файл определения модуля Контекста доставки:
module com.baeldung.dddmodules.shippingcontext {
requires com.baeldung.dddmodules.sharedkernel;
exports com.baeldung.dddmodules.shippingcontext.service;
exports com.baeldung.dddmodules.shippingcontext.model;
exports com.baeldung.dddmodules.shippingcontext.repository;
provides com.baeldung.dddmodules.shippingcontext.service.ShippingService
with com.baeldung.dddmodules.shippingcontext.service.ParcelShippingService;
}Таким же образом мы экспортируем реализацию по умолчанию для интерфейса Службы доставки|/.
4.4. Инфраструктурный модуль
Теперь пришло время описать модуль инфраструктуры. Этот модуль содержит сведения о реализации определенных интерфейсов. Мы начнем с создания простой реализации интерфейса EventBus :
public class SimpleEventBus implements EventBus {
private final Map> subscribers = new ConcurrentHashMap<>();
@Override
public void publish(E event) {
if (subscribers.containsKey(event.getType())) {
subscribers.get(event.getType())
.forEach(subscriber -> subscriber.onEvent(event));
}
}
@Override
public void subscribe(String eventType, EventSubscriber subscriber) {
Set eventSubscribers = subscribers.get(eventType);
if (eventSubscribers == null) {
eventSubscribers = new CopyOnWriteArraySet<>();
subscribers.put(eventType, eventSubscribers);
}
eventSubscribers.add(subscriber);
}
@Override
public void unsubscribe(String eventType, EventSubscriber subscriber) {
if (subscribers.containsKey(eventType)) {
subscribers.get(eventType).remove(subscriber);
}
}
} Далее нам нужно реализовать интерфейсы Репозиторий заказов клиентов и Репозиторий заказов на доставку|/. В большинстве случаев сущность Order будет храниться в той же таблице, но использоваться в качестве другой модели сущности в ограниченных контекстах.
Очень часто можно увидеть одну сущность, содержащую смешанный код из разных областей бизнес-домена или низкоуровневые сопоставления баз данных. Для нашей реализации мы разделили наши сущности в соответствии с ограниченными контекстами: Заказ клиента и Отгружаемый заказ .
Во-первых, давайте создадим класс, который будет представлять всю постоянную модель:
public static class PersistenceOrder {
public int orderId;
public String paymentMethod;
public String address;
public List orderItems;
public static class OrderItem {
public int productId;
public float unitPrice;
public float itemWeight;
public int quantity;
}
} Мы видим, что этот класс содержит все поля как из Заказа клиента , так и из Отгружаемого заказа сущностей.
Чтобы все было просто, давайте смоделируем базу данных в памяти:
public class InMemoryOrderStore implements CustomerOrderRepository, ShippingOrderRepository {
private Map ordersDb = new HashMap<>();
@Override
public void saveCustomerOrder(CustomerOrder order) {
this.ordersDb.put(order.getOrderId(), new PersistenceOrder(order.getOrderId(),
order.getPaymentMethod(),
order.getAddress(),
order
.getOrderItems()
.stream()
.map(orderItem ->
new PersistenceOrder.OrderItem(orderItem.getProductId(),
orderItem.getQuantity(),
orderItem.getUnitWeight(),
orderItem.getUnitPrice()))
.collect(Collectors.toList())
));
}
@Override
public Optional findShippableOrder(int orderId) {
if (!this.ordersDb.containsKey(orderId)) return Optional.empty();
PersistenceOrder orderRecord = this.ordersDb.get(orderId);
return Optional.of(
new ShippableOrder(orderRecord.orderId, orderRecord.orderItems
.stream().map(orderItem -> new PackageItem(orderItem.productId,
orderItem.itemWeight,
orderItem.quantity * orderItem.unitPrice)
).collect(Collectors.toList())));
}
} Здесь мы сохраняем и извлекаем различные типы сущностей путем преобразования постоянных моделей в соответствующий тип или из него.
Наконец, давайте создадим определение модуля:
module com.baeldung.dddmodules.infrastructure {
requires transitive com.baeldung.dddmodules.sharedkernel;
requires transitive com.baeldung.dddmodules.ordercontext;
requires transitive com.baeldung.dddmodules.shippingcontext;
provides com.baeldung.dddmodules.sharedkernel.events.EventBus
with com.baeldung.dddmodules.infrastructure.events.SimpleEventBus;
provides com.baeldung.dddmodules.ordercontext.repository.CustomerOrderRepository
with com.baeldung.dddmodules.infrastructure.db.InMemoryOrderStore;
provides com.baeldung.dddmodules.shippingcontext.repository.ShippingOrderRepository
with com.baeldung.dddmodules.infrastructure.db.InMemoryOrderStore;
}Используя предложение provides with , мы предоставляем реализацию нескольких интерфейсов, которые были определены в других модулях.
Кроме того, этот модуль действует как агрегатор зависимостей, поэтому мы используем ключевое слово requires transitive . В результате модуль, которому требуется модуль инфраструктуры, транзитивно получит все эти зависимости.
4.5. Основной модуль
В заключение давайте определим модуль, который будет точкой входа в наше приложение:
module com.baeldung.dddmodules.mainapp {
uses com.baeldung.dddmodules.sharedkernel.events.EventBus;
uses com.baeldung.dddmodules.ordercontext.service.OrderService;
uses com.baeldung.dddmodules.ordercontext.repository.CustomerOrderRepository;
uses com.baeldung.dddmodules.shippingcontext.repository.ShippingOrderRepository;
uses com.baeldung.dddmodules.shippingcontext.service.ShippingService;
requires transitive com.baeldung.dddmodules.infrastructure;
}Поскольку мы только что установили транзитивные зависимости для модуля инфраструктуры, нам не нужно требовать их здесь явно.
С другой стороны, мы перечисляем эти зависимости с помощью ключевого слова uses . Предложение uses указывает ServiceLoader , о котором мы узнаем в следующей главе, что этот модуль хочет использовать эти интерфейсы. Однако для этого не требуется, чтобы реализации были доступны во время компиляции.
5. Запуск приложения
Наконец, мы почти готовы к созданию нашего приложения. Мы будем использовать Maven для создания нашего проекта. Это значительно облегчает работу с модулями.
5.1. Структура проекта
Наш проект содержит пять модулей и родительский модуль . Давайте взглянем на структуру нашего проекта:
ddd-modules (the root directory)
pom.xml
|-- infrastructure
|-- src
|-- main
| -- java
module-info.java
|-- com.baeldung.dddmodules.infrastructure
pom.xml
|-- mainapp
|-- src
|-- main
| -- java
module-info.java
|-- com.baeldung.dddmodules.mainapp
pom.xml
|-- ordercontext
|-- src
|-- main
| -- java
module-info.java
|--com.baeldung.dddmodules.ordercontext
pom.xml
|-- sharedkernel
|-- src
|-- main
| -- java
module-info.java
|-- com.baeldung.dddmodules.sharedkernel
pom.xml
|-- shippingcontext
|-- src
|-- main
| -- java
module-info.java
|-- com.baeldung.dddmodules.shippingcontext
pom.xml5.2. Основное применение
К настоящему времени у нас есть все, кроме основного приложения, поэтому давайте определим наш метод main :
public static void main(String args[]) {
Map, Object> container = createContainer();
OrderService orderService = (OrderService) container.get(OrderService.class);
ShippingService shippingService = (ShippingService) container.get(ShippingService.class);
shippingService.listenToOrderEvents();
CustomerOrder customerOrder = new CustomerOrder();
int orderId = 1;
customerOrder.setOrderId(orderId);
List orderItems = new ArrayList();
orderItems.add(new OrderItem(1, 2, 3, 1));
orderItems.add(new OrderItem(2, 1, 1, 1));
orderItems.add(new OrderItem(3, 4, 11, 21));
customerOrder.setOrderItems(orderItems);
customerOrder.setPaymentMethod("PayPal");
customerOrder.setAddress("Full address here");
orderService.placeOrder(customerOrder);
if (orderId == shippingService.getParcelByOrderId(orderId).get().getOrderId()) {
System.out.println("Order has been processed and shipped successfully");
}
} Давайте вкратце обсудим наш основной метод. В этом методе мы моделируем простой поток заказов клиентов, используя ранее определенные услуги. Сначала мы создали заказ из трех предметов и предоставили необходимую информацию о доставке и оплате. Затем мы отправили заказ и, наконец, проверили, был ли он успешно отправлен и обработан.
Но как мы получили все зависимости и почему метод createContainer возвращает Map, Object>? Давайте подробнее рассмотрим этот метод.
5.3. Внедрение Зависимостей С Помощью ServiceLoader
В этом проекте у нас нет никаких зависимостей Spring IoC , поэтому в качестве альтернативы мы будем использовать API ServiceLoader для обнаружения реализаций служб. Это не новая функция — сам API ServiceLoader существует с Java 6.
Мы можем получить экземпляр загрузчика, вызвав один из статических методов load класса ServiceLoader . Метод load возвращает тип Iterable , чтобы мы могли перебирать обнаруженные реализации.
Теперь давайте применим загрузчик для разрешения наших зависимостей:
public static Map, Object> createContainer() { EventBus eventBus = ServiceLoader.load(EventBus.class).findFirst().get(); CustomerOrderRepository customerOrderRepository = ServiceLoader.load(CustomerOrderRepository.class) .findFirst().get(); ShippingOrderRepository shippingOrderRepository = ServiceLoader.load(ShippingOrderRepository.class) .findFirst().get(); ShippingService shippingService = ServiceLoader.load(ShippingService.class).findFirst().get(); shippingService.setEventBus(eventBus); shippingService.setOrderRepository(shippingOrderRepository); OrderService orderService = ServiceLoader.load(OrderService.class).findFirst().get(); orderService.setEventBus(eventBus); orderService.setOrderRepository(customerOrderRepository); HashMap , Object> container = new HashMap<>(); container.put(OrderService.class, orderService); container.put(ShippingService.class, shippingService); return container; }
Здесь мы вызываем метод static load для каждого нужного нам интерфейса, который каждый раз создает новый экземпляр загрузчика. В результате он не будет кэшировать уже разрешенные зависимости — вместо этого он будет каждый раз создавать новые экземпляры.
Как правило, экземпляры служб могут быть созданы одним из двух способов. Либо класс реализации службы должен иметь открытый конструктор no-arg, либо он должен использовать статический метод provider .
Как следствие, большинство наших сервисов не имеют конструкторов и методов установки зависимостей. Но, как мы уже видели, класс In Memory Order Store реализует два интерфейса: Хранилище заказов клиентов и ShippingOrderRepository .
Однако, если мы запросим каждый из этих интерфейсов с помощью метода load , мы получим разные экземпляры InMemoryOrderStore . Это нежелательное поведение, поэтому давайте использовать метод provider для кэширования экземпляра:
public class InMemoryOrderStore implements CustomerOrderRepository, ShippingOrderRepository {
private volatile static InMemoryOrderStore instance = new InMemoryOrderStore();
public static InMemoryOrderStore provider() {
return instance;
}
}Мы применили шаблон Singleton для кэширования одного экземпляра класса In Memory Order Store и возврата его из метода provider .
Если поставщик услуг объявляет метод provider , то ServiceLoader вызывает этот метод для получения экземпляра службы. В противном случае он попытается создать экземпляр с помощью конструктора без аргументов через Reflection . В результате мы можем изменить механизм поставщика услуг, не затрагивая наш метод createContainer .
И, наконец, мы предоставляем разрешенные зависимости службам через сеттеры и возвращаем настроенные службы.
Наконец, мы можем запустить приложение.
6. Заключение
В этой статье мы обсудили некоторые критические концепции DDD: Ограниченный контекст, Вездесущий язык и сопоставление контекста. Хотя разделение системы на ограниченные контексты имеет много преимуществ, в то же время нет необходимости применять этот подход везде.
Далее мы рассмотрели, как использовать систему модулей Java 9 вместе с ограниченным контекстом для создания сильно инкапсулированных модулей.
Кроме того, мы рассмотрели механизм обнаружения зависимостей по умолчанию ServiceLoader .
Полный исходный код проекта доступен на GitHub .