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 ListorderItems; 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); Mappayload = 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 ListpackageItems; }
В этом случае сущность не содержит поля Способ оплаты . Это потому, что в нашем контексте доставки нам все равно, какой способ оплаты используется. Контекст доставки отвечает только за обработку отгрузок заказов.
Кроме того, объект Parcel специфичен для контекста доставки:
public class Parcel { private int orderId; private String address; private String trackingId; private ListpackageItems; 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 MapshippedParcels = 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 { defaultvoid 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 ListorderItems; public static class OrderItem { public int productId; public float unitPrice; public float itemWeight; public int quantity; } }
Мы видим, что этот класс содержит все поля как из Заказа клиента , так и из Отгружаемого заказа сущностей.
Чтобы все было просто, давайте смоделируем базу данных в памяти:
public class InMemoryOrderStore implements CustomerOrderRepository, ShippingOrderRepository { private MapordersDb = 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.xml
5.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 .