1. 领域驱动设计(DDD)基础:核心概念与战略设计
领域驱动设计(Domain-Driven Design,DDD)是一套应对复杂业务系统建模的方法论,由 Eric Evans 在其经典著作中提出。它强调以业务领域为核心,通过统一的语言(Ubiquitous Language)将业务概念精确映射到软件模型中。本章作为课程的开篇,将系统梳理 DDD 的核心概念与战略设计工具,为后续的模块边界划分与审批流故障排查奠定坚实的理论基础。
学习提示:DDD 并非银弹,它最适合业务逻辑复杂、规则多变的领域。对于简单的 CRUD 系统,过度使用 DDD 反而会增加不必要的复杂度。请在学习过程中始终思考:“这个模式解决了我当前业务中的哪个痛点?”
1.1 核心概念:战术建模的基石
战术建模(Tactical Modeling)提供了构建领域模型的具体模式。理解这些模式是进行有效领域建模的第一步。
| 概念 | 定义 | 关键特征 | 典型示例 |
|---|---|---|---|
| 实体(Entity) | 具有唯一标识,且其状态会随时间变化的对象。 | 有 ID、可变、通过 ID 区分 | 用户(userId)、订单(orderId) |
| 值对象(Value Object) | 描述事物的属性,没有唯一标识,不可变。 | 无 ID、不可变、通过属性值相等判断 | 地址(Address)、金额(Money) |
| 聚合(Aggregate) | 一组相关对象的集合,由聚合根(Aggregate Root)统一管理。 | 有聚合根、内部一致性、外部只能通过聚合根访问 | 订单聚合(Order 为根,包含 OrderItem) |
| 领域事件(Domain Event) | 领域中发生的、业务人员关心的、具有业务意义的事件。 | 过去时、不可变、携带上下文数据 | 订单已支付(OrderPaid)、库存已扣减 |
实战辨析:实体 vs 值对象
判断一个概念是实体还是值对象,核心标准是:你是否关心它的“身份”变化?
- 一个“人”换了地址,他仍然是同一个人 → 人是实体,地址是值对象。
- 两个“金额”都是 100 元,它们完全等价 → 金额是值对象。
- 如果系统需要追踪“地址”的历史变更记录(如物流轨迹),地址就可能升级为实体。
判断一个概念是实体还是值对象,核心标准是:你是否关心它的“身份”变化?
- 一个“人”换了地址,他仍然是同一个人 → 人是实体,地址是值对象。
- 两个“金额”都是 100 元,它们完全等价 → 金额是值对象。
- 如果系统需要追踪“地址”的历史变更记录(如物流轨迹),地址就可能升级为实体。
1.2 聚合:事务一致性的边界
聚合是 DDD 中最重要的战术模式之一,它定义了数据一致性边界。在一个聚合内部,所有对象的修改必须在一次事务中完成并保证强一致性;跨聚合的修改则通过最终一致性实现。
// 聚合根示例:Order(订单)
public class Order {
private OrderId id; // 实体标识
private List<OrderItem> items; // 内部实体列表
private Money totalAmount; // 值对象
private OrderStatus status; // 值对象
// 外部只能通过聚合根的方法修改内部状态
public void addItem(ProductId productId, int quantity, Money price) {
// 业务规则校验:商品是否可售、数量是否超限
this.items.add(new OrderItem(productId, quantity, price));
this.totalAmount = this.calculateTotal(); // 重新计算总额
}
public void pay() {
// 状态机校验:只有待支付状态才能支付
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单状态不允许支付");
}
this.status = OrderStatus.PAID;
// 发布领域事件
DomainEventPublisher.publish(new OrderPaidEvent(this.id));
}
}
常见误区:不要将聚合设计得过大。一个聚合包含的对象越多,事务冲突的概率就越高,系统并发性能越差。遵循“一次操作只修改一个聚合”的原则,优先考虑小聚合。
1.3 领域事件:解耦与异步通知
领域事件用于在聚合之间或限界上下文之间传递信息,实现松耦合。当一个聚合完成某个业务操作后,它会发布一个事件,其他感兴趣的订阅者可以异步响应。
- 发布者:通常是聚合根,在方法执行成功后发布事件。
- 订阅者:可以是同一个限界上下文内的其他聚合,也可以是不同限界上下文的服务。
- 典型场景:订单支付成功后,发布
OrderPaidEvent,库存服务订阅后扣减库存,通知服务订阅后发送短信。
1.4 战略设计:划定系统边界
当系统规模变大,单一模型无法覆盖所有业务时,就需要引入战略设计(Strategic Design)。战略设计的核心是限界上下文(Bounded Context)和上下文映射(Context Map)。
1.5 限界上下文:模型的边界
限界上下文是 DDD 战略设计的核心。它定义了一个显式的边界,在这个边界内部,领域模型、通用语言和代码实现保持高度一致。不同限界上下文之间通过明确的接口进行通信。
- 每个限界上下文拥有自己的通用语言:同一个术语在不同上下文中可能有不同含义。例如,“客户”在“销售上下文”中可能指购买者,在“售后上下文”中可能指报修人。
- 每个限界上下文拥有自己的模型和代码库:团队可以独立演进,技术栈也可以不同。
- 典型划分依据:业务能力(如订单、库存、支付)、子域类型(核心域、支撑域、通用域)。
1.6 上下文映射:协作模式
上下文映射描述了不同限界上下文之间的协作关系。常见的映射模式包括:
| 模式 | 关系描述 | 适用场景 |
|---|---|---|
| 合作关系(Partnership) | 两个上下文紧密协作,共同演进。 | 两个团队需要同步开发,共享同一个目标。 |
| 共享内核(Shared Kernel) | 共享一部分模型和代码,保持一致性。 | 两个上下文有高度重叠的核心概念。 |
| 客户-供应商(Customer-Supplier) | 上游(供应商)决定模型,下游(客户)适配。 | 下游依赖上游的接口,上游优先满足下游需求。 |
| 防腐层(Anti-Corruption Layer) | 下游上下文通过一层转换,隔离上游模型的污染。 | 集成遗留系统或第三方服务,保护自身模型。 |
| 开放主机服务(Open Host Service) | 上游提供明确的 API 供下游调用。 | 上游希望被多个下游以标准方式集成。 |
| 发布语言(Published Language) | 使用标准的数据格式(如 XML、JSON Schema)进行通信。 | 跨系统集成,需要统一的数据契约。 |
| 大泥球(Big Ball of Mud) | 没有清晰边界,模型混乱耦合。 | 需要避免的反模式。 |
战略设计核心原则:
1. 按业务能力划分:每个限界上下文应该对应一个独立的业务能力或子域。
2. 保持模型纯净:在限界上下文内部,模型只反映该上下文的业务逻辑,不掺杂其他上下文的规则。
3. 明确通信方式:根据上下文之间的关系,选择合适的映射模式(如防腐层、开放主机服务)。
1. 按业务能力划分:每个限界上下文应该对应一个独立的业务能力或子域。
2. 保持模型纯净:在限界上下文内部,模型只反映该上下文的业务逻辑,不掺杂其他上下文的规则。
3. 明确通信方式:根据上下文之间的关系,选择合适的映射模式(如防腐层、开放主机服务)。
1.7 本章小结
本章介绍了 DDD 的两大支柱:战术建模(实体、值对象、聚合、领域事件)和战略设计(限界上下文、上下文映射)。这些概念是后续进行模块边界划分和审批流故障排查的基础。请务必理解:
- 聚合是保证数据一致性的最小单元,也是微服务拆分的候选边界。
- 限界上下文是微服务划分的逻辑依据,上下文映射决定了服务间的通信模式。
- 领域事件是实现服务间异步解耦的关键手段。
课后思考:在你的当前项目中,是否存在“大泥球”式的模块?尝试用限界上下文重新划分边界,并思考每个上下文应该使用哪种映射模式进行协作。