DDD领域驱动设计实践_订单物流
原有的对DDD的学习笔记:
https://blog.csdn.net/qinwenjng120/article/details/150874301《面试之微服务架构》
https://blog.csdn.net/qinwenjng120/article/details/150557401《电商项目_微服务_架构》
项目背景:星海电商平台
公司最初是一个单体应用,随着业务发展,订单和物流模块变得异常复杂且难以维护。每次促销活动,系统都濒临崩溃,开发新功能(如“预约配送”、“冷链物流”)举步维艰。我们决定采用DDD对“订单”和“物流”进行重构。
说明:正常设计一个复杂的软件系统,想到的是微服务的拆分,那么拆分成哪些微服务?拆分微服务的原则是什么?DDD就是一个指导思想。
第一步:战略设计 - 划定问题边界(确定 限界上下文)
目的:确认关键业务场景、关键实体、值对象,从而确定限界上下文。
1. 事件风暴与统一语言
我们组织产品、运营、业务、开发等人员一起进行“事件风暴”(业务关键场景确认)。
-
我们列出了一些核心领域事件(Domain Events):
-
订单已提交 -
订单已支付 -
订单已发货 -
包裹已出库 -
包裹已送达分拣中心 -
包裹已开始派送 -
订单已签收
-
-
在这些事件中,我们提炼出统一语言(Ubiquitous Language):
-
订单(Order): 用户的购买意向,包含商品、价格、收货地址。核心状态 是
待支付、已支付、已取消。 -
物流单(Shipping): 实际的物理包裹流转记录。核心状态是
待出库、运输中、派送中、已签收。 -
包裹(Parcel): 一个物流单可能包含多个包裹。
-
分拣中心(SortingCenter): 物流网络中的关键节点。
-
2. 划定限界上下文(Bounded Context)
通过分析事件和命令,我们发现“订单”和“物流”关注的核心问题完全不同:
-
订单上下文(Order Context): 关心钱和交易。它负责计算金额、管理支付状态、处理优惠券。它不关心包裹具体怎么运输。
-
物流上下文(Shipping Context): 关心物和移动。它负责规划路线、更新包裹位置、预估送达时间。它不关心订单用了什么优惠券。
因此,我们明确地划分出两个限界上下文:Order Context 和 Shipping Context。
3. 上下文映射(Context Map)
这两个上下文紧密相关,那么它们如何协作呢?
我们采用 “客户-供应商”模式。
-
客户: Shipping Context(物流上下文)
-
供应商: Order Context(订单上下文)
物流系统需要从订单系统获取“收货地址”和“商品清单”来创建物流单。订单上下文作为供应商,需要提供一个稳定、明确的接口(如RESTful API或事件)给物流上下文消费。
第二步:战术设计 - 构建解决方案模型
现在,我们深入到每个限界上下文内部,进行精细的建模, 确定每个限界上下文内的 实体、值对象、聚合、聚合根、领域服务。
1. 在 Order Context 内部:
-
实体(Entity):
Order(订单)是一个典型的实体,它有唯一的ID(orderId),并且它的状态会随时间变化(从待支付到已支付)。 -
值对象(Value Object):
Address(收货地址)、OrderItem(订单项)。两个订单即使所有商品都一样,也是不同的订单(实体);但两个相同的地址(省市区街道一样)则可以认为是同一个值对象,可以互换。 -
聚合根(Aggregate Root):
Order本身就是聚合根。我们不会直接操作OrderItem,必须通过Order这个根来操作,比如order.addItem(product, quantity)。这保证了订单数据的一致性(例如,添加商品时自动重新计算总价)。(判断能直接操作的实体是什么) -
领域服务(Domain Service):
OrderPaymentService(订单支付服务)。支付这个行为涉及与外部支付网关的交互,它不属于任何一个实体或值对象的固有职责,因此用一个领域服务来封装。 -
领域事件(Domain Event):
OrderPaidEvent(订单已支付事件)。当支付成功后,订单聚合根会发布这个事件。这个事件是连接两个上下文的关键!
代码实现上,所谓聚合根是能够直接操作的对象, 就是定义订单类, 定义其包括的实体和值对象,定义订单相关的操作。
// 聚合根
public class Order {
private OrderId id;
private Address shippingAddress;
private List<OrderItem> items;
private OrderStatus status;
// 核心业务逻辑:提交订单
public void submit() {
// 验证逻辑...
this.status = OrderStatus.PENDING_PAYMENT;
}
// 核心业务逻辑:支付成功
public void paySuccess() {
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalOrderOperationException("订单状态异常,无法支付");
}
this.status = OrderStatus.PAID;
// 发布领域事件!
DomainEventPublisher.publish(new OrderPaidEvent(this.id, this.shippingAddress, this.items));
}
// 通过根来添加商品,保证业务规则
public void addItem(Product product, int quantity) {
// 检查商品、库存等...
this.items.add(new OrderItem(product, quantity));
this.calculateTotal(); // 内部重新计算总价
}
}
2. 在 Shipping Context 内部:
-
实体(Entity):
Shipping(物流单),有唯一IDshippingId。Parcel(包裹)也可能是实体。 -
值对象(Value Object):
Location(位置信息)、RoutePlan(路线规划)。 -
聚合根(Aggregate Root):
Shipping是聚合根。它内部维护着包裹的当前状态和位置历史。 -
领域服务(Domain Service):
RoutePlanningService(路线规划服务),它根据地址和物流网络计算最优路径,这是一个复杂的领域逻辑。 -
应用服务(Application Service):
ShippingAppService。它是领域的门面,接收外部指令(如创建物流单)。 -
事件处理器(EventHandler): 它订阅来自Order Context的
OrderPaidEvent。
// 聚合根
public class Shipping {
private ShippingId id;
private OrderId orderId; // 关联的订单ID
private Location currentLocation;
private List<ShippingHistory> history;
private ShippingStatus status;
public void updateLocation(Location newLocation) {
this.history.add(new ShippingHistory(this.currentLocation, Instant.now()));
this.currentLocation = newLocation;
// 可以根据规则更新状态,例如进入某个分拣中心后变为“运输中”
if (newLocation.isSortingCenter()) {
this.status = ShippingStatus.IN_TRANSIT;
}
}
}
// 应用服务
@Service
public class ShippingAppService {
@Autowired
private ShippingRepository repository;
@Autowired
private RoutePlanningService routePlanningService;
// 事件处理器:当收到订单已支付事件时,创建物流单
@EventListener
public void handle(OrderPaidEvent event) {
// 1. 调用基础设施层,通过API从Order Context获取详细的配送信息(防腐层)
OrderDeliveryInfo deliveryInfo = orderServiceClient.getDeliveryInfo(event.getOrderId());
// 2. 调用领域服务进行路线规划
RoutePlan plan = routePlanningService.calculatePlan(deliveryInfo.getAddress());
// 3. 创建物流聚合根
Shipping shipping = new Shipping(
ShippingId.generate(),
event.getOrderId(),
deliveryInfo,
plan
);
// 4. 通过仓储持久化
repository.save(shipping);
}
// 更新物流位置
public void updateLocation(ShippingId id, Location newLocation) {
Shipping shipping = repository.findById(id);
shipping.updateLocation(newLocation); // 调用聚合根的方法
repository.save(shipping);
}
}
第三步:架构与实现
我们采用 微服务架构 来匹配限界上下文:
-
订单服务 (Order Service): 对应 Order Context。
-
物流服务 (Shipping Service): 对应 Shipping Context。
它们之间通过 消息中间件(如RabbitMQ/Kafka) 进行异步的事件驱动通信。
-
订单服务在支付成功后,发布
OrderPaidEvent到消息队列。 -
物流服务监听该事件,然后创建物流单。
数据库分离: 每个服务拥有自己独立的数据库,订单服务不直接访问物流服务的表,反之亦然。这实现了真正的解耦。
更多推荐


所有评论(0)