深入浅出 DDD(领域驱动设计) - 指南
引言
随着电商系统业务复杂度的提升,传统 CRUD 或事务脚本式开发模式已经无法满足需求。订单、库存、支付、促销活动等业务高度耦合,导致代码逻辑分散、维护成本高、业务迭代慢。
领域驱动设计(DDD, Domain-Driven Design)的核心目标,是让代码准确表达业务逻辑,让业务逻辑沉淀在正确的位置。
本文将结合电商系统,系统梳理 DDD 成功落地的必要因素,并结合具体实践建议,帮助你构建可维护、高内聚、低耦合的系统架构。
一、限界上下文(Bounded Context)
定义:限界上下文是业务子域的边界,内部使用统一的业务语言和模型。
为什么必要:
- 避免不同模块间语言混乱
- 明确团队和模块职责
- 支持跨模块协作而不产生耦合
电商示例:
电商系统
├── 商品上下文(Product BC):管理商品信息、规格、价格
├── 订单上下文(Order BC):管理订单创建、修改、支付状态
├── 库存上下文(Inventory BC):库存扣减、冻结、回滚
└── 支付上下文(Payment BC):第三方支付接口、支付记录
每个上下文内部语义一致,但上下文之间可以对同一概念有不同模型表达。例如“订单状态”在订单上下文和支付上下文中意义不同:订单上下文可能关注“待支付、已支付、已发货”,支付上下文则关注“支付中、支付成功、支付失败”。
实践建议:
- 不要试图把整个系统统一成一个模型,否则聚合会膨胀、逻辑复杂
- 根据业务能力划分上下文,结合团队边界和系统模块划分
二、聚合(Aggregate)与聚合根(Aggregate Root)
定义:聚合是一组相关对象的集合,聚合根是唯一入口,负责保证内部一致性。每个聚合 只有一个聚合根,聚合内部可能有多个实体或值对象,但对外只有聚合根暴露接口。
为什么必要:
- 保证聚合内部状态一致
- 避免外部直接修改内部对象
- 聚合是业务逻辑的最小单位
电商示例:
Order 聚合:
├── Order(聚合根)
├── OrderItem(实体)
├── Price(值对象)
└── ShippingAddress(值对象)
业务行为示例:
addItem():添加订单项applyDiscount():应用优惠券或满减活动pay():支付订单close():关闭订单
这些行为必须通过聚合根执行,任何外部服务不能直接修改 OrderItem 或 Price。
实践建议:
- 聚合不宜过大,每个聚合应保证强一致性内部可控
- 聚合之间通过事件或应用服务交互,避免跨聚合事务
三、实体(Entity)和值对象(Value Object)
实体(Entity):具有唯一标识,状态可变
值对象(Value Object):无唯一标识,不可变,表示业务属性
为什么必要:
- 代码清晰表达业务概念
- 业务逻辑易理解、维护
电商示例:
- 实体:Order、Product、Customer、Warehouse
- 值对象:Money、Address、SKU、Quantity
示例:Money 值对象(Java):
public class Money {
private final BigDecimal value;
private final String currency;
public Money(BigDecimal value, String currency) {
if (value.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负");
}
this.value = value;
this.currency = currency;
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("货币单位不一致");
}
return new Money(this.value.add(other.value), this.currency);
}
}
使用值对象可以把零散的校验逻辑封装在一个地方,减少业务代码重复。
四、领域服务(Domain Service)
定义:领域服务是独立于聚合的业务逻辑封装,用于处理跨聚合或无法归属单个聚合的操作。
为什么必要:
- 处理跨聚合业务逻辑
- 避免聚合臃肿
- 让业务行为有明确归属
电商示例:
PriceCalculationService:订单最终价格计算(原价 + 优惠券 + 活动折扣 + 会员折扣)PaymentDomainService:处理支付过程,包括订单状态校验、调用第三方支付接口、支付结果回调
实践建议:
- 领域服务应只负责业务逻辑,不直接处理持久化或外部接口
- 聚合内部的逻辑尽量放在聚合根,领域服务只处理“跨聚合行为”
五、应用服务(Application Service)
定义:应用服务负责流程编排,不承载业务规则。
为什么必要:
- 对外提供接口
- 调用聚合或领域服务完成业务流程
- 发布领域事件、调用仓储
电商示例:
PlaceOrderApplicationService:下单流程CancelOrderApplicationService:取消订单流程
示例代码:
public void placeOrder(PlaceOrderCommand cmd) {
List<Product> products = productRepository.findByIds(cmd.productIds);
// 调用聚合根创建订单
Order order = Order.create(cmd.customerId, products, cmd.address);
// 保存订单
orderRepository.save(order);
// 发布订单创建事件
eventBus.publish(new OrderCreated(order.getId()));
}
应用服务只负责串流程,业务逻辑应在聚合或领域服务中处理。
六、领域事件(Domain Event)
定义:领域事件记录领域中发生的重要事件,用于模块间解耦和异步协作。
为什么必要:
- 保持高内聚、低耦合
- 支持业务扩展而不破坏核心逻辑
- 支持事件驱动架构
电商示例:
OrderCreated→ InventoryService.freezeStock()PaymentSucceeded→ UpdateOrderStatus → NotifyDeliveryOrderDelivered→ AddCustomerLoyaltyPoints
实践建议:
- 事件应是“业务上有意义的事实”,而不是系统实现细节
- 可以结合消息队列实现异步处理,提高系统可扩展性
七、总结:DDD 的必要因素
成功落地 DDD,需要理解并实践以下核心概念:
- 限界上下文:划定业务边界,统一业务语言
- 聚合与聚合根:保证聚合内部一致性
- 实体与值对象:准确表达业务概念
- 领域服务:跨聚合业务逻辑
- 应用服务:流程编排
- 领域事件:模块解耦与异步协作
缺少任意一项,DDD 的价值难以体现,系统容易退化为传统 CRUD 或事务脚本架构,导致业务逻辑分散、聚合贫血、系统难维护。
八、落地建议
- 从业务领域出发,先划分上下文,再设计聚合
- 聚合根负责业务一致性,跨聚合操作通过领域服务和应用服务实现
- 使用领域事件解耦模块,避免直接调用其他聚合内部逻辑
- 值对象封装业务规则,减少重复逻辑
- 按上下文设计团队职责,实现业务与代码边界一致
在中大型电商系统中,DDD 可以帮助你构建可维护、可扩展、符合真实业务语言的架构。
浙公网安备 33010602011771号