DDD:有界上下文和Java模块 -- 领域事件

1,590 阅读21分钟

1.概述

领域驱动设计(DDD)是一组原则和工具,可帮助我们设计有效的软件体系结构以提供更高的业务价值。 通过将整个应用程序领域划分为多个语义一致的部分,Bounded Context是从架构的泥潭中拯救体系结构的核心和必不可少的模式之一。

同时,借助Java 9 Module System,我们可以创建高度封装的模块。

在本教程中,我们将创建一个简单的store应用程序,并了解如何在定义有限上下文的显式边界时利用Java 9模块。

2. DDD有界上下文

如今,软件系统已不是简单的CRUD应用程序。 实际上,典型的整体式企业系统由一些旧代码库和新添加的功能组成。 但是,每次进行更改时,维护这样的系统变得越来越困难。 最终,它可能变得完全无法维护。

2.1. 限界上下文和通用语言

为了解决这个问题解决,DDD提供限界上下文的概念。 有界上下文是特定条款和规则一致适用的领域的逻辑边界。 在此边界内,所有术语,定义和概念都构成了通用语言。

特别是,通用语言的主要好处是将来自特定业务领域不同领域的项目成员分组在一起。 此外,多个上下文可能对同一事物起作用。 但是,在每个上下文中它可能具有不同的含义。

2.2. 订单上下文[Order Context]

让我们开始通过定义订单上下文来实现我们的应用程序。 该上下文包含两个领域模型:OrderItem和CustomerOrder。

CustomerOrder领域模型是一个聚合根:

package com.ecommerce.dddmodules.ordercontext.model;

import java.util.List;
/**
 * @packageName: ordercontext.model(订单上下文.领域包)
 * @className: CustomerOrder(客户订单领域)
 * @description: 围绕此订单领域展开业务
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/01
 */
public class CustomerOrder {
	/**
	 * 订单ID
	 */
	private int orderId;
	/**
	 * 付款方式
	 */
	private String paymentMethod;
	/**
	 * 地址
	 */
	private String address;
	/**
	 * 订单项目列表
	 */
	private List<OrderItem> orderItems;
	/**
	 * 默认构造器
	 */
	public CustomerOrder() { }

	/**
	 * 计算总价
	 * @return 返回总价
	 */
	public float calculateTotalPrice() {
		return orderItems.stream().map(OrderItem::getTotalPrice)
				.reduce(0F, Float::sum);
	}

	/**
	 * 获取订单ID
	 * @return 返回订单ID
	 */
	public int getOrderId() {
		return orderId;
	}

	/**
	 * 设置订单ID
	 * @param orderId 订单ID
	 */
	public void setOrderId(int orderId) {
		this.orderId = orderId;
	}

	/**
	 * 获取订单项目明细
	 * @return 订单项目明细
	 */
	public List<OrderItem> getOrderItems() {
		return orderItems;
	}

	/**
	 * 设置订单项目明细
	 * @param orderItems 订单项目明细
	 */
	public void setOrderItems(List<OrderItem> orderItems) {
		this.orderItems = orderItems;
	}

	/**
	 * 获取付款方式
	 * @return 返回付款方式
	 */
	public String getPaymentMethod() {
		return paymentMethod;
	}

	/**
	 * 设置付款方式
	 * @param paymentMethod 付款方式
	 */
	public void setPaymentMethod(String paymentMethod) {
		this.paymentMethod = paymentMethod;
	}

	/**
	 * 获取地址
	 * @return 返回地址
	 */
	public String getAddress() {
		return address;
	}

	/**
	 * 设置地址
	 * @param address 地址
	 */
	public void setAddress(String address) {
		this.address = address;
	}
}

如我们所见,该类包含calculateTotalPrice业务方法。 但是,在现实世界的项目中,它可能会更加复杂-例如,在最终价格中包括折扣和税收。

接下来,让我们创建OrderItem类:

package com.ecommerce.dddmodules.ordercontext.model;
/**
 * @packageName: ordercontext.model(订单上下文.领域包)
 * @className: OrderItem(订单项目领域)
 * @description: 围绕此订单项目领域展开业务
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/01
 */
public class OrderItem {
	/**
	 * 产品ID
	 */
	private int productId;
	/**
	 * 数量
	 */
	private int quantity;
	/**
	 * 单价
	 */
	private float unitPrice;
	/**
	 * 单位重量
	 */
	private float unitWeight;

	/**
	 * 通过产品ID、数量、单价、单位重量
	 * 构造订单项目
	 * @param productId 产品ID
	 * @param quantity 数量
	 * @param unitPrice 单价
	 * @param unitWeight 单位重量
	 */
	public OrderItem(int productId, int quantity, float unitPrice, float unitWeight) {
		this.productId = productId;
		this.quantity = quantity;
		this.unitPrice = unitPrice;
		this.unitWeight = unitWeight;
	}

	/**
	 * 获取产品ID
	 * @return 返回产品ID
	 */
	public int getProductId() {
		return productId;
	}

	/**
	 * 获取数量
	 * @return 返回数量
	 */
	public int getQuantity() {
		return quantity;
	}

	/**
	 * 获取总价
	 * @return 返回总价
	 */
	public float getTotalPrice() {
		return this.quantity * this.unitPrice;
	}

	/**
	 * 获取单位重量
	 * @return 返回单位重量
	 */
	public float getUnitWeight() {
		return unitWeight;
	}

	/**
	 * 获取单价
	 * @return 返回单价
	 */
	public float getUnitPrice() {
		return unitPrice;
	}
}

我们已经定义了领域模型,但是我们还需要向应用程序的其他部分公开一些API。 让我们创建CustomerOrderService类:

package com.ecommerce.dddmodules.ordercontext.service;

import com.ecommerce.dddmodules.ordercontext.model.CustomerOrder;
import com.ecommerce.dddmodules.ordercontext.repository.CustomerOrderRepository;
import com.ecommerce.dddmodules.sharedkernel.events.ApplicationEvent;
import com.ecommerce.dddmodules.sharedkernel.events.EventBus;

import java.util.HashMap;
import java.util.Map;
/**
 * @packageName: ordercontext.service(订单上下文.服务包)
 * @className: CustomerOrderService(客户订单服务实现)
 * @description: 实现订单服务接口
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/01
 */
public class CustomerOrderService implements OrderService {
	/**
	 * 订单准备装运事件
	 */
	public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent";

	/**
	 * 客户订单存储库
	 */
	private CustomerOrderRepository orderRepository;

	/**
	 * 事件总线
	 */
	private EventBus eventBus;

	/**
	 * 下订单
	 * @param order 订单
	 */
	@Override
	public void placeOrder(CustomerOrder order) {
		// 首先保存订单
		this.orderRepository.saveCustomerOrder(order);
		// 创造有效载荷量保存空间
		Map<String, String> payload = new HashMap<>();

		// 将下的订单ID保存到此空间中,
		payload.put("order_id", String.valueOf(order.getOrderId()));

		// 把保存有订单ID的有效载荷量保存空间,导入到应用事件
		ApplicationEvent event = new ApplicationEvent(payload) {
			@Override
			public String getType() {
				return EVENT_ORDER_READY_FOR_SHIPMENT;
			}
		};
		// 把应用事件发布到事件总线
		this.eventBus.publish(event);
	}

	/**
	 * 获取事件总线
	 * @return 返回事件总线
	 */
	@Override
	public EventBus getEventBus() {
		return eventBus;
	}

	/**
	 * 设置事件总线
	 * @param eventBus 事件总线
	 */
	@Override
	public void setEventBus(EventBus eventBus) {
		this.eventBus = eventBus;
	}

	/**
	 * 设置订单存储库
	 * @param orderRepository 订单存储库
	 */
	public void setOrderRepository(CustomerOrderRepository orderRepository) {
		this.orderRepository = orderRepository;
	}
}

在这里,我们要强调一些要点。 placeOrder方法负责处理客户订单。 处理订单后,事件将发布到EventBus。 我们将在下一章中讨论事件驱动的通信。 该服务为OrderService接口提供默认实现:

package com.ecommerce.dddmodules.ordercontext.service;

import com.ecommerce.dddmodules.ordercontext.model.CustomerOrder;
import com.ecommerce.dddmodules.ordercontext.repository.CustomerOrderRepository;
import com.ecommerce.dddmodules.sharedkernel.service.ApplicationService;
/**
 * @packageName: ordercontext.service(订单上下文.服务包)
 * @className: OrderService(客户订单服务接口)
 * @description: 扩展应用服务接口
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/01
 */
public interface OrderService extends ApplicationService {
	/**
	 * 下订单
	 * @param order 客户订单
	 */
	void placeOrder(CustomerOrder order);

	/**
	 * 设置客户订单存储库
	 * @param orderRepository
	 */
	void setOrderRepository(CustomerOrderRepository orderRepository);
}

此外,此服务要求CustomerOrderRepository保留订单:

package com.ecommerce.dddmodules.ordercontext.repository;

import com.ecommerce.dddmodules.ordercontext.model.CustomerOrder;
/**
 * @packageName: ordercontext.repository(订单上下文.存储库包)
 * @className: CustomerOrderRepository(客户订单存储库)
 * @description: 围绕此客户订单,为持久化而做的存储库
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/01
 */
public interface CustomerOrderRepository {
	/**
	 * 保存客户订单
	 * @param order 客户订单
	 */
	void saveCustomerOrder(CustomerOrder order);
}

至关重要的是,该接口不是在此上下文中实现的,而是由基础设施模块提供的,我们将在后面看到。

2.3. 运输上下文[Shipping Context]

现在,让我们定义“发货上下文”。 它也很简单,并包含三个领域模型:Parcel(包裹),PackageItem(包裹项目)和ShippableOrder(可发货订单)。

image.png

让我们从ShippableOrder领域模型开始:

package com.ecommerce.dddmodules.shippingcontext.model;

import java.util.List;
/**
 * @packageName: shippingcontext.model(发货上下文.领域包)
 * @className: ShippableOrder(可发货订单领域)
 * @description: 围绕此可发货订单领域展开业务
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/01
 */
public class ShippableOrder {
	/**
	 * 订单ID
	 */
	private int orderId;

	/**
	 * 送货地址
	 */
	private String address;

	/**
	 * 包裹项目明细
	 */
	private List<PackageItem> packageItems;

	/**
	 * 由订单ID、包裹项目明细构造可发货订单
	 * @param orderId
	 * @param packageItems
	 */
	public ShippableOrder(int orderId, List<PackageItem> packageItems) {
		this.orderId = orderId;
		this.packageItems = packageItems;
	}

	/**
	 * 获取订单ID
	 * @return 订单ID
	 */
	public int getOrderId() {
		return orderId;
	}

	/**
	 * 设置订单ID
	 * @param orderId 订单ID
	 */
	public void setOrderId(int orderId) {
		this.orderId = orderId;
	}

	/**
	 * 获取包裹项目明细
	 * @return 返回包裹项目明细
	 */
	public List<PackageItem> getPackageItems() {
		return packageItems;
	}

	/**
	 * 设置包裹项目明细
	 * @param packageItems 包裹项目明细
	 */
	public void setPackageItems(List<PackageItem> packageItems) {
		this.packageItems = packageItems;
	}

	/**
	 * 获取发货地址
	 * @return 发货地址
	 */
	public String getAddress() {
		return address;
	}

	/**
	 * 设置发货地址
	 * @param address 发货地址
	 */
	public void setAddress(String address) {
		this.address = address;
	}
}

在这种情况下,领域模型不包含PaymentMethod字段。 这是因为,在我们的发货上下文内,我们不在乎使用哪种付款方式。 发货上下文仅负责处理订单装运。

另外,包裹领域是特定于发货上下文的:

package com.ecommerce.dddmodules.shippingcontext.model;

import java.util.List;
/**
 * @packageName: shippingcontext.model(发货上下文.领域包)
 * @className: Parcel(包裹领域)
 * @description: 围绕此包裹领域展开业务
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/01
 */
public class Parcel {
	/**
	 * 订单ID
	 */
	private int orderId;
	/**
	 * 送货地址
	 */
	private String address;
	/**
	 * 跟踪号码
	 */
	private String trackingId;
	/**
	 * 包裹项目明细
	 */
	private List<PackageItem> packageItems;

	/**
	 * 由订单ID、送货地址、包裹项目明细构造包裹
	 * @param orderId 订单ID
	 * @param address 送货地址
	 * @param packageItems 包裹项目明细
	 */
	public Parcel(int orderId, String address, List<PackageItem> packageItems) {
		this.orderId = orderId;
		this.address = address;
		this.packageItems = packageItems;
	}

	/**
	 * 计算总重量
	 * @return 总重量
	 */
	public float calculateTotalWeight() {
		return packageItems.stream().map(PackageItem::getWeight)
				.reduce(0F, Float::sum);
	}

	/**
	 * 是否可纳税
	 * @return 返回是否可纳税
	 */
	public boolean isTaxable() {
		// 通过估值计算,大于100时可纳税;否则不用纳税
		return calculateEstimatedValue() > 100;
	}

	/**
	 * 计算估值
	 * @return 返回估值
	 */
	public float calculateEstimatedValue() {
		return packageItems.stream().map(PackageItem::getWeight)
				.reduce(0F, Float::sum);
	}

	/**
	 * 获取订单ID
	 * @return 返回订单ID
	 */
	public int getOrderId() {
		return orderId;
	}

	/**
	 * 设置订单ID
	 * @param orderId 订单ID
	 */
	public void setOrderId(int orderId) {
		this.orderId = orderId;
	}

	/**
	 * 获取跟踪号码
	 * @return 返回跟踪号码
	 */
	public String getTrackingId() {
		return trackingId;
	}

	/**
	 * 设置跟踪号码
	 * @param trackingId 跟踪号码
	 */
	public void setTrackingId(String trackingId) {
		this.trackingId = trackingId;
	}

	/**
	 * 获取送货地址
	 * @return 返回送货地址
	 */
	public String getAddress() {
		return address;
	}

	/**
	 * 设置送货地址
	 * @param address 送货地址
	 */
	public void setAddress(String address) {
		this.address = address;
	}
}

如我们所见,它还包含特定的业务方法,并充当聚合根。 最后,让我们定义ParcelShippingService:

package com.ecommerce.dddmodules.shippingcontext.service;

import com.ecommerce.dddmodules.sharedkernel.events.ApplicationEvent;
import com.ecommerce.dddmodules.sharedkernel.events.EventBus;
import com.ecommerce.dddmodules.sharedkernel.events.EventSubscriber;
import com.ecommerce.dddmodules.shippingcontext.model.Parcel;
import com.ecommerce.dddmodules.shippingcontext.model.ShippableOrder;
import com.ecommerce.dddmodules.shippingcontext.repository.ShippingOrderRepository;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
 * @packageName: shippingcontext.service(发货上下文.服务包)
 * @className: ParcelShippingService(包裹发货服务实现)
 * @description: 实现发货服务接口
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/02
 */
public class ParcelShippingService implements ShippingService {
	/**
	 * 订单准备装运事件
	 */
	public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent";

	/**
	 * 发货单存储库
	 */
	private ShippingOrderRepository orderRepository;

	/**
	 * 事件总线
	 */
	private EventBus eventBus;

	/**
	 * 已发货包裹对照表
	 */
	private Map<Integer, Parcel> shippedParcels = new HashMap<>();

	/**
	 * 根据订单ID进行发货(具体实现)
	 * @param orderId 订单ID
	 */
	@Override
	public void shipOrder(int orderId) {
		Optional<ShippableOrder> 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 <E extends ApplicationEvent> void onEvent(E event) {
				shipOrder(Integer.parseInt(event.getPayloadValue("order_id")));
			}
		});
	}
	/**
	 * 根据订单ID获取包裹信息(具体实现)
	 * @param orderId 订单ID
	 * @return 返回包裹信息
	 */
	@Override
	public Optional<Parcel> getParcelByOrderId(int orderId) {
		// 对应订单ID的包裹如果有,直接返回;否则返回空
		return Optional.ofNullable(this.shippedParcels.get(orderId));
	}
	/**
	 * 设置订单存储库(具体实现)
	 * @param orderRepository 订单存储库
	 */
	@Override
	public void setOrderRepository(ShippingOrderRepository orderRepository) {
		this.orderRepository = orderRepository;
	}
	/**
	 * 获取事件总线(具体指定)
	 * @return 返回事件总线
	 */
	@Override
	public EventBus getEventBus() {
		return eventBus;
	}
	/**
	 * 设置事件总线(具体指定)
	 * @param eventBus 事件总线
	 */
	@Override
	public void setEventBus(EventBus eventBus) {
		this.eventBus = eventBus;
	}


}

该服务类似地使用ShippingOrderRepository通过id来获取订单。 更重要的是,它订阅了OrderReadyForShipmentEvent事件,该事件由另一个上下文发布。 发生此事件时,服务将应用一些规则并发送订单。 为了简单起见,我们将装运的订单存储在HashMap中。

3.上下文映射

到目前为止,我们定义了两个上下文。 但是,我们没有在它们之间设置任何明确的关系。 为此,DDD具有上下文映射的概念。 上下文映射是系统不同上下文之间关系的直观描述。 此图显示了不同部分如何共存在一起以形成领域。

有界上下文之间的关系有五种主要类型:

  • 伙伴关系–两种环境之间的关系,可以相互协作以使两个团队具有相互依赖的目标

  • 共享内核–一种将多个上下文的公共部分提取到另一个上下文/模块以减少代码重复的一种关系

  • 客户-供应商–两个上下文之间的连接,其中一个上下文(上游)产生数据,而另一个上下文(下游)消耗数据。 在这种关系中,双方都希望建立最佳的沟通渠道

  • 遵从者-这种关系也有上游和下游,但是下游始终符合上游的API

  • 反腐层–这种类型的关系广泛用于遗留系统,以使其适应新的体系结构并逐渐从遗留代码库迁移。 反腐层充当适配器,以转换来自上游的数据并保护免受意外更改

在我们的特定示例中,我们将使用共享内核关系。 我们不会以其纯粹的形式对其进行定义,但是它将主要充当系统中事件的中介者。

因此,SharedKernel模块将不包含任何具体的实现,而仅包含接口。 让我们从EventBus接口开始:

package com.ecommerce.dddmodules.sharedkernel.events;
/**
 * @packageName: sharedkernel.events(共享内核.事件包)
 * @className: EventBus(事件总线接口)
 * @description: 抽离事件总线接口
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/01
 */
public interface EventBus {
	/**
	 * 发布(事件)
	 * @param event 事件
	 * @param <E> 限定继承自应用事件
	 */
	<E extends ApplicationEvent> void publish(E event);

	/**
	 * 订阅(事件)
	 * @param eventType 事件类型
	 * @param subscriber 订阅者
	 * @param <E> 限定继承自应用事件
	 */
	<E extends ApplicationEvent> void subscribe(String eventType, EventSubscriber subscriber);

	/**
	 * 退订(事件)
	 * @param eventType 事件类型
	 * @param subscriber 订阅者
	 * @param <E> 限定继承自应用事件
	 */
	<E extends ApplicationEvent> void unsubscribe(String eventType, EventSubscriber subscriber);
}

该接口将在稍后的“基础结构”模块中实现。

接下来,我们使用默认方法创建一个基础服务接口,以支持事件驱动的通信:

package com.ecommerce.dddmodules.sharedkernel.service;

import com.ecommerce.dddmodules.sharedkernel.events.ApplicationEvent;
import com.ecommerce.dddmodules.sharedkernel.events.EventBus;
import com.ecommerce.dddmodules.sharedkernel.events.EventSubscriber;
/**
 * @packageName: sharedkernel.service(共享内核.服务包)
 * @className: ApplicationService(应用服务接口)
 * @description: 抽离应用服务接口
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/01
 */
public interface ApplicationService {

	/**
	 * 发布事件(带有默认实现部分)
	 * @param event 事件
	 * @param <E> 继承自应用事件
	 */
	default <E extends ApplicationEvent> void publishEvent(E event) {
		// 获取事件总线
		EventBus eventBus = getEventBus();
		if (eventBus != null) {
			// 把事件发布到事件总线
			eventBus.publish(event);
		}
	}

	/**
	 * 订阅事件(带有默认实现部分)
	 * @param eventType 事件类型
	 * @param subscriber 订阅者
	 * @param <E> 继承自应用事件
	 */
	default <E extends ApplicationEvent> void subscribe(String eventType, EventSubscriber subscriber) {
		// 获取事件总线
		EventBus eventBus = getEventBus();
		if (eventBus != null) {
			// 从事件总线订阅事件
			eventBus.subscribe(eventType, subscriber);
		}
	}

	/**
	 * 退订事件(带有默认实现部分)
	 * @param eventType 事件类型
	 * @param subscriber 订阅者
	 * @param <E> 继承自应用事件
	 */
	default <E extends ApplicationEvent> void unsubscribe(String eventType, EventSubscriber subscriber) {
		// 获取事件总线
		EventBus eventBus = getEventBus();
		if (eventBus != null) {
			// 从事件总线退订事件
			eventBus.unsubscribe(eventType, subscriber);
		}
	}

	/**
	 * 获取事件总线
	 * @return 返回事件总线
	 */
	EventBus getEventBus();

	/**
	 * 设置事件总线
	 * @param eventBus 事件总线
	 */
	void setEventBus(EventBus eventBus);
}

因此,有界上下文中的服务接口将该接口扩展为具有与事件相关的公共功能。

4. Java 9模块化

现在,该探讨Java 9 Module System如何支持已定义的应用程序结构。

Java平台模块系统(JPMS)鼓励构建更可靠和高度封装的模块。 因此,这些功能可以帮助隔离我们的环境并建立清晰的边界。

让我们看一下我们的最终模块图:

4.1. 共享内核模块

让我们从SharedKernel模块开始,该模块与其他模块没有任何依赖关系。 module-info.java如下:

/**
 * @module: com.ecommerce.dddmodules.sharedkernel(共享内核)
 * @description: 列出共享内核工程中,要分享的包
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/02
 */
module com.ecommerce.dddmodules.sharedkernel {
	/**
	 * 导出:[共享内核.事件]包
	 */
	exports com.ecommerce.dddmodules.sharedkernel.events;
	/**
	 * 导出:[共享内核.服务]包
	 */
	exports com.ecommerce.dddmodules.sharedkernel.service;
}

我们导出模块接口,因此它们可用于其他模块。

4.2. OrderContext模块

接下来,让我们将重点转移到OrderContext模块。 它只需要在SharedKernel模块中定义的接口:

/**
 * @module: com.ecommerce.dddmodules.ordercontext(订单上下文)
 * @description: 列出此模块依赖那些模块,导出那些包以及指定接口的默认实现
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/02
 */
module com.ecommerce.dddmodules.ordercontext {
	/**
	 * 依赖:com.ecommerce.dddmodules.sharedkernel(共享内核模块)
	 */
	requires com.ecommerce.dddmodules.sharedkernel;
	/**
	 * 导出:[订单上下文.服务]包
	 */
	exports com.ecommerce.dddmodules.ordercontext.service;
	/**
	 * 导出:[订单上下文.领域]包
	 */
	exports com.ecommerce.dddmodules.ordercontext.model;
	/**
	 * 导出:[订单上下文.存储库]包
	 */
	exports com.ecommerce.dddmodules.ordercontext.repository;
	/**
	 * 指定 OrderService接口的默认实现 CustomerOrderService
	 */
	provides com.ecommerce.dddmodules.ordercontext.service.OrderService
			with com.ecommerce.dddmodules.ordercontext.service.CustomerOrderService;
}

此外,我们可以看到该模块导出了OrderService接口的默认实现。

4.3. ShippingContext模块

与上一个模块类似,让我们创建ShippingContext模块定义文件:

/**
 * @module: com.ecommerce.dddmodules.shippingcontext(发货上下文)
 * @description: 列出此模块依赖那些模块,导出那些包以及指定接口的默认实现
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/02
 */
module com.ecommerce.dddmodules.shippingcontext {
	/**
	 * 依赖:com.ecommerce.dddmodules.sharedkernel(共享内核模块)
	 */
	requires com.ecommerce.dddmodules.sharedkernel;
	/**
	 * 导出:[发货上下文.服务]包
	 */
	exports com.ecommerce.dddmodules.shippingcontext.service;
	/**
	 * 导出:[发货上下文.领域]包
	 */
	exports com.ecommerce.dddmodules.shippingcontext.model;
	/**
	 * 导出:[发货上下文.存储库]包
	 */
	exports com.ecommerce.dddmodules.shippingcontext.repository;
	/**
	 * 指定 ShippingService接口的默认实现 ParcelShippingService
	 */
	provides com.ecommerce.dddmodules.shippingcontext.service.ShippingService
			with com.ecommerce.dddmodules.shippingcontext.service.ParcelShippingService;
}

以同样的方式,我们导出了ShippingService接口的默认实现。

4.4. 基础设施模块

现在该描述基础设施模块了。 该模块包含已定义接口的实现详细信息。 我们将从为EventBus接口创建一个简单的实现开始:

package com.ecommerce.dddmodules.infrastructure.events;

import com.ecommerce.dddmodules.sharedkernel.events.ApplicationEvent;
import com.ecommerce.dddmodules.sharedkernel.events.EventBus;
import com.ecommerce.dddmodules.sharedkernel.events.EventSubscriber;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
/**
 * @packageName: infrastructure.events(基础设施.事件包)
 * @className: SimpleEventBus(简单事件总线)
 * @description: 事件总线的最简单实现方式
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/02
 */
public class SimpleEventBus implements EventBus {
	/**
	 * 订阅者存储空间
	 */
	private final Map<String, Set<EventSubscriber>> subscribers = new ConcurrentHashMap<>();

	/**
	 * 发布事件
	 * @param event 事件
	 * @param <E> 限定事件为应用事件
	 */
	@Override
	public <E extends ApplicationEvent> void publish(E event) {
		//如果订阅者存储空间已经包含此事件类型,则依次激活此类型的事件
		if (subscribers.containsKey(event.getType())) {
			subscribers.get(event.getType())
					.forEach(subscriber -> subscriber.onEvent(event));
		}
	}

	/**
	 * 根据类型和订阅者进行事件订阅
	 * @param eventType 事件类型
	 * @param subscriber 订阅者
	 * @param <E> 限定事件为应用事件
	 */
	@Override
	public <E extends ApplicationEvent> void subscribe(String eventType, EventSubscriber subscriber) {
		// 从订阅者存储空间,获得所有此类型的订阅者集合
		Set<EventSubscriber> eventSubscribers = subscribers.get(eventType);
		// 如果此空间没有该类型的订阅者集合,则重新生成一个;放到此类型中
		if (eventSubscribers == null) {
			eventSubscribers = new CopyOnWriteArraySet<>();
			subscribers.put(eventType, eventSubscribers);
		}
		// 然后在此类型的订阅者集合,追加此订阅者
		eventSubscribers.add(subscriber);
	}

	/**
	 * 根据类型和订阅者进行事件退订
	 * @param eventType 事件类型
	 * @param subscriber 订阅者
	 * @param <E> 限定事件为应用事件
	 */
	@Override
	public <E extends ApplicationEvent> void unsubscribe(String eventType, EventSubscriber subscriber) {
		// 如果订阅者存储空间,包含此类型;则获取该类型的订阅者集合,并从集合中移除该订阅者
		if (subscribers.containsKey(eventType)) {
			subscribers.get(eventType).remove(subscriber);
		}
	}
}

接下来,我们需要实现CustomerOrderRepository和ShippingOrderRepository接口。 在大多数情况下,Order领域模型将存储在同一表中,但在有界上下文中用作不同的领域模型。

常见的情况是,单个领域模型包含来自业务领域不同领域或低级数据库映射的混合代码。 对于我们的实现,我们根据有界上下文(CustomerOrder和ShippableOrder)拆分了领域模型。

首先,让我们创建一个代表整个持久模型的类:

	/**
	 * @packageName: infrastructure.db(基础设施.数据库包)
	 * @className: PersistenceOrder(持久化订单)
	 * @description: 实现客户订单存储库和发货上下文存储库
	 * @author: luds
	 * @version: v1.0
	 * @date: 2021-04/02
	 */
	public static class PersistenceOrder {
		/**
		 * 订单ID
		 */
		public int orderId;
		/**
		 * 付款方式
		 */
		public String paymentMethod;
		/**
		 * 送货地址
		 */
		public String address;
		/**
		 * 订单项目明细
		 */
		public List<OrderItem> orderItems;

		/**
		 * 由订单ID、付款方式、送货地址、订单项目明细构造持久化订单
		 * @param orderId 订单ID
		 * @param paymentMethod 付款方式
		 * @param address 送货地址
		 * @param orderItems 订单项目明细
		 */
		public PersistenceOrder(int orderId, String paymentMethod, String address, List<OrderItem> orderItems) {
			this.orderId = orderId;
			this.paymentMethod = paymentMethod;
			this.address = address;
			this.orderItems = orderItems;
		}

		/**
		 * 内部静态订单项目类
		 */
		public static class OrderItem {
			/**
			 * 产品ID
			 */
			public int productId;
			/**
			 * 单价
			 */
			public float unitPrice;
			/**
			 * 单位重量
			 */
			public float unitWeight;
			/**
			 * 数量
			 */
			public int quantity;

			/**
			 * 由产品ID、数量、单位重量、单价构造订单项目
			 * @param productId 产品ID
			 * @param quantity 数量
			 * @param unitWeight 单位重量
			 * @param unitPrice 单价
			 */
			public OrderItem(int productId, int quantity, float unitWeight, float unitPrice) {
				this.unitWeight = unitWeight;
				this.quantity = quantity;
				this.unitPrice = unitPrice;
				this.productId = productId;
			}
		}
	}

我们可以看到该类包含CustomerOrder和ShippableOrder领域模型的所有字段。 为简单起见,让我们模拟一个内存数据库:

package com.ecommerce.dddmodules.infrastructure.db;

import com.ecommerce.dddmodules.ordercontext.model.CustomerOrder;
import com.ecommerce.dddmodules.ordercontext.repository.CustomerOrderRepository;
import com.ecommerce.dddmodules.shippingcontext.model.PackageItem;
import com.ecommerce.dddmodules.shippingcontext.model.ShippableOrder;
import com.ecommerce.dddmodules.shippingcontext.repository.ShippingOrderRepository;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
 * @packageName: infrastructure.db(基础设施.数据库包)
 * @className: InMemoryOrderStore(内存订单存储)
 * @description: 实现客户订单存储库和发货上下文存储库
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/02
 */
public class InMemoryOrderStore implements CustomerOrderRepository, ShippingOrderRepository {
	/**
	 * 定义一个静态各线程间共享的 内存订单存储单例
	 */
	private volatile static InMemoryOrderStore instance = new InMemoryOrderStore();

	/**
	 * 定义一个模拟的持久化订单存储空间
	 */
	private Map<Integer, PersistenceOrder> ordersDb = new HashMap<>();

	/**
	 * 获取一个静态各线程可共享的单例
	 * @return 返回一个静态各线程可共享的单例
	 */
	public static InMemoryOrderStore provider() {
		return instance;
	}

	/**
	 * 存储客户订单(实现)
	 * @param order 客户订单
	 */
	@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())
		));
	}
	/**
	 * 根据订单ID,查找可发货订单(实现)
	 * @param orderId 客户订单
	 */
	@Override
	public Optional<ShippableOrder> 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.unitWeight,
								orderItem.quantity * orderItem.unitPrice)
						).collect(Collectors.toList())));
	}
 }

在这里,我们通过将持久性模型转换为适当的类型或从其转换为持久性并检索不同类型的领域模型。

最后,让我们创建模块定义:

/**
 * @module: com.ecommerce.dddmodules.infrastructure(订单上下文)
 * @description: 列出此模块依赖那些模块,导出那些包以及指定接口的默认实现
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/02
 */
module com.ecommerce.dddmodules.infrastructure {
	/**
	 * 依赖传递:com.ecommerce.dddmodules.sharedkernel[共享内核]
	 */
	requires transitive com.ecommerce.dddmodules.sharedkernel;
	/**
	 * 依赖传递:com.ecommerce.dddmodules.ordercontext[订单上下文]
	 */
	requires transitive com.ecommerce.dddmodules.ordercontext;
	/**
	 * 依赖传递:com.ecommerce.dddmodules.shippingcontext[发货上下文]
	 */
	requires transitive com.ecommerce.dddmodules.shippingcontext;
	/**
	 * 指定 EventBus接口的默认实现 SimpleEventBus
	 */
	provides com.ecommerce.dddmodules.sharedkernel.events.EventBus
			with com.ecommerce.dddmodules.infrastructure.events.SimpleEventBus;
	/**
	 * 指定 CustomerOrderRepository接口的默认实现 InMemoryOrderStore
	 */
	provides com.ecommerce.dddmodules.ordercontext.repository.CustomerOrderRepository
			with com.ecommerce.dddmodules.infrastructure.db.InMemoryOrderStore;
	/**
	 * 指定 ShippingOrderRepository接口的默认实现 InMemoryOrderStore
	 */
	provides com.ecommerce.dddmodules.shippingcontext.repository.ShippingOrderRepository
			with com.ecommerce.dddmodules.infrastructure.db.InMemoryOrderStore;
}

通过使用Provides子句,我们提供了其他模块中定义的一些接口的实现。

此外,该模块还充当依赖项的聚集器,因此我们使用require transitive关键字。 结果,需要基础结构模块的模块将可传递地获得所有这些依赖关系。

4.5. 主模块

最后,让我们定义一个模块,该模块将成为我们应用程序的入口点:

/**
 * @module: com.ecommerce.dddmodules.mainapp(主应用模块)
 * @description: 列出此模块使用哪些接口,以及依赖什么
 * @author: luds
 * @version: v1.0
 * @date: 2021-04/02
 */
module com.ecommerce.dddmodules.mainapp {
	/**
	 * 使用:共享内核.事件.事件总线接口
	 */
	uses com.ecommerce.dddmodules.sharedkernel.events.EventBus;
	/**
	 * 使用:订单上下文.服务.订单服务接口
	 */
	uses com.ecommerce.dddmodules.ordercontext.service.OrderService;
	/**
	 * 使用:订单上下文.存储库.客户订单存储库接口
	 */
	uses com.ecommerce.dddmodules.ordercontext.repository.CustomerOrderRepository;
	/**
	 * 使用:发货上下文.存储库.发货单存储库接口
	 */
	uses com.ecommerce.dddmodules.shippingcontext.repository.ShippingOrderRepository;
	/**
	 * 使用:发货上下文.服务.发货服务接口
	 */
	uses com.ecommerce.dddmodules.shippingcontext.service.ShippingService;
	/**
	 * 依赖传递:基础设施模块
	 */
	requires transitive com.ecommerce.dddmodules.infrastructure;
}

由于我们刚刚在基础设施模块上设置了传递依赖项,因此我们无需在此处明确要求它们。

另一方面,我们使用uses关键字列出这些依赖关系。 Uses子句指示ServiceLoader(我们将在下一章中发现)该模块要使用这些接口。 但是,它不需要在编译时就可以使用实现。

5.运行应用程序

最后,我们几乎已经准备好构建我们的应用程序。 我们将利用Maven来构建我们的项目。 这使得使用模块更加容易。

5.1. 项目结构

我们的项目包含五个模块和父模块。 让我们看一下我们的项目结构:

ddd-modules (the root directory)
pom.xml
|-- infrastructure
    |-- src
        |-- main
            | -- java
            module-info.java
            |-- com.ecommerce.dddmodules.infrastructure
    pom.xml
|-- mainapp
    |-- src
        |-- main
            | -- java
            module-info.java
            |-- com.ecommerce.dddmodules.mainapp
    pom.xml
|-- ordercontext
    |-- src
        |-- main
            | -- java
            module-info.java
            |--com.ecommerce.dddmodules.ordercontext
    pom.xml
|-- sharedkernel
    |-- src
        |-- main
            | -- java
            module-info.java
            |-- com.ecommerce.dddmodules.sharedkernel
    pom.xml
|-- shippingcontext
    |-- src
        |-- main
            | -- java
            module-info.java
            |-- com.ecommerce.dddmodules.shippingcontext
    pom.xml

5.2. 主要用途

到目前为止,除了主应用程序之外,我们已经拥有所有其他东西,因此让我们定义我们的main方法:

	/**
	 * 服务启动入口
	 * @param args
	 */
	public static void main(String args[]) {
		// 创建需用的容器
		Map<Class<?>, Object> container = createContainer();

		// 从容器中获取订单服务
		OrderService orderService = (OrderService) container.get(OrderService.class);

		// 从容器中获取发货服务
		ShippingService shippingService = (ShippingService) container.get(ShippingService.class);

		// 发货服务监听订购事件
		shippingService.listenToOrderEvents();

		// 模拟演示一个客户订单
		CustomerOrder customerOrder = new CustomerOrder();

		/**
		 * 此客户订单:订单号为1,订单项目明细分别为如下:
		 * - 产品ID:1,数量:2,单价:3,单位重量:1
		 * - 产品ID:2,数量:1,单价:1,单位重量:1
		 * - 产品ID:3,数量:4,单价:11,单位重量:21
		 * 付款方式:PayPal
		 * 送货地址:Full address here
		 * 使用订单服务下此客户订单
		 */
		int orderId = 1;
		customerOrder.setOrderId(orderId);
		List<OrderItem> orderItems = new ArrayList<OrderItem>();
		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);
		// 从发货单服务,根据此订单ID获取包裹后再取订单ID;如果取到的订单ID和客户订单ID相同,就说明发货成功了
		if (orderId == shippingService.getParcelByOrderId(orderId).get().getOrderId()) {
			System.out.println("Order has been processed and shipped successfully");
		}
	}

让我们简要讨论一下我们的主要方法。 在这种方法中,我们通过使用先前定义的服务来模拟简单的客户订单流程。 首先,我们创建了包含三个项目的订单,并提供了必要的运输和付款信息。 接下来,我们提交了订单,并最终检查了该订单是否已成功发货和处理。

但是我们如何获得所有依赖关系,为什么createContainer方法返回Map <Class <?>,Object>? 让我们仔细看一下这种方法。

5.3. 使用ServiceLoader进行依赖注入

在此项目中,我们没有任何Spring IoC依赖性,因此,我们将使用ServiceLoader API来发现服务的实现。 这不是一个新功能-自Java 6起就出现了ServiceLoader API本身。

我们可以通过调用ServiceLoader类的静态加载方法之一来获得一个加载器实例。 load方法返回Iterable类型,以便我们可以迭代发现的实现。

现在,让我们应用加载器来解决我们的依赖关系:

	/**
	 * 创建容器
	 * @return 返回容器
	 */
	public static Map<Class<?>, Object> createContainer() {
		// 使用ServiceLoader装载事件总线
		EventBus eventBus = ServiceLoader.load(EventBus.class).findFirst().get();

		// 使用ServiceLoader装载客户订单存储库
		CustomerOrderRepository customerOrderRepository = ServiceLoader.load(CustomerOrderRepository.class).findFirst().get();
		// 使用ServiceLoader装载发货单存储库
		ShippingOrderRepository shippingOrderRepository = ServiceLoader.load(ShippingOrderRepository.class).findFirst().get();
		// 使用ServiceLoader装载发货单服务
		ShippingService shippingService = ServiceLoader.load(ShippingService.class).findFirst().get();
		// 发货单服务设置事件总线
		shippingService.setEventBus(eventBus);
		// 发货单服务设置发货单存储库
		shippingService.setOrderRepository(shippingOrderRepository);

		// 使用ServiceLoader装载订单服务
		OrderService orderService = ServiceLoader.load(OrderService.class).findFirst().get();
		// 订单服务设置事件总线
		orderService.setEventBus(eventBus);
		// 订单服务设置客户订单存储库
		orderService.setOrderRepository(customerOrderRepository);

		// 创建一个容器
		HashMap<Class<?>, Object> container = new HashMap<>();

		// 在容器中,放入订单服务
		container.put(OrderService.class, orderService);

		// 在容器中,放入发货服务
		container.put(ShippingService.class, shippingService);

		// 返回此容器
		return container;
	}

在这里,我们为所需的每个接口调用静态加载方法,该方法每次都会创建一个新的加载器实例。 结果,它不会缓存已经解决的依赖关系,而是每次都会创建新的实例。

通常,可以通过以下两种方式之一创建服务实例。 服务实现类必须具有公共的无参数构造函数,或者必须使用静态提供程序方法。

因此,我们的大多数服务都具有无参数构造函数和用于依赖项的setter方法。 但是,正如我们已经看到的那样,InMemoryOrderStore类实现了两个接口:CustomerOrderRepository和ShippingOrderRepository。

但是,如果我们使用load方法请求这些接口中的每个接口,我们将获得InMemoryOrderStore的不同实例。 这不是理想的行为,因此让我们使用提供者方法技术来缓存实例:

public class InMemoryOrderStore implements CustomerOrderRepository, ShippingOrderRepository {
    private volatile static InMemoryOrderStore instance = new InMemoryOrderStore();

    public static InMemoryOrderStore provider() {
        return instance;
    }
}

我们已经应用了Singleton模式来缓存InMemoryOrderStore类的单个实例,并从provider方法中返回它。

如果服务提供者声明了提供者方法,则ServiceLoader调用此方法以获取服务的实例。 否则,它将尝试通过反射使用无参数构造函数创建实例。 结果,我们可以更改服务提供者机制,而不会影响我们的createContainer方法。

最后,我们通过设置器提供对服务的已解决依赖关系,并返回已配置的服务。

最后,我们可以运行该应用程序。

6. 结论

在本文中,我们讨论了一些关键的DDD概念:绑定上下文,通用语言和上下文映射。 尽管将系统划分为“边界上下文”有很多好处,但与此同时,无需在所有地方都采用这种方法。

接下来,我们已经看到了如何使用Java 9 Module System和Bounded Context来创建高度封装的模块。

此外,我们介绍了用于发现依赖项的默认ServiceLoader机制。

该项目的完整源代码可在gitee.com/actual-comb… 上获得。

参考:www.baeldung.com/java-module…