4. 领域驱动设计(DDD)入门:战略与战术设计
在复杂的企业业务系统中,模块边界模糊、业务逻辑散落是导致审批流配置混乱的根本原因。领域驱动设计(DDD)提供了一套从业务出发的建模方法,帮助我们在“业务复杂度”与“技术实现”之间建立清晰的映射。本章聚焦 DDD 的两大核心阶段:战略设计(划定边界)与战术设计(构造模型),为后续的模块拆分与参数优化奠定理论基础。
4.1 战略设计:划定业务边界与统一语言
战略设计的核心目标是解决“系统该拆成几块”以及“团队之间如何沟通”的问题。它不关心具体代码,而是关注业务领域的划分与协作。
4.1.1 限界上下文(Bounded Context)
限界上下文是 DDD 中最关键的边界概念。它定义了一个显式的边界,在边界内部,领域模型、术语、业务规则保持严格一致;边界之间通过明确的接口进行通信。每个限界上下文对应一个独立的业务子系统或微服务。
例如,在电商系统中,“订单”在“销售上下文”中代表待支付的交易记录,而在“物流上下文”中则代表已打包的包裹。强行统一这两个“订单”会导致模型臃肿且难以维护。
| 限界上下文 | 核心职责 | 典型术语示例 |
|---|---|---|
| 销售上下文 | 处理客户下单、支付、促销 | 购物车、订单、优惠券 |
| 库存上下文 | 管理商品库存、出入库 | SKU、库存量、仓库 |
| 物流上下文 | 安排配送、跟踪包裹 | 运单、配送站、签收 |
| 审批上下文 | 定义审批流程、处理审批任务 | 审批模板、审批节点、审批人 |
4.1.2 通用语言(Ubiquitous Language)
通用语言是团队(业务专家、产品、开发、测试)共同使用的业务语言。它必须基于限界上下文,确保在同一个上下文内,所有人对同一个词的理解完全一致。通用语言应直接体现在代码、文档、讨论和用户故事中。
- 来源: 从业务专家的术语中提炼,而非技术术语。
- 载体: 代码中的类名、方法名、变量名;需求文档;测试用例。
- 维护: 当业务规则变化时,通用语言必须同步演进。
下图展示了战略设计中限界上下文与通用语言的关系:
4.2 战术设计:构造领域模型
在战略设计划定的边界内,战术设计指导我们如何用代码精确表达业务逻辑。核心构造块包括:实体、值对象、聚合、领域服务、领域事件等。本节重点介绍前三个最基础的元素。
4.2.1 实体(Entity)
实体是具有唯一标识且会经历生命周期变化的对象。它的核心特征是“身份”,而非属性。即使两个实体的所有属性都相同,只要 ID 不同,它们就是不同的对象。
- 标识: 通常由数据库 ID、UUID 或业务主键(如订单号)表示。
- 可变性: 实体的属性可以改变(如订单状态从“待支付”变为“已支付”),但身份不变。
- 示例: 用户(User)、订单(Order)、审批单(ApprovalRequest)。
// 实体示例:审批单
public class ApprovalRequest {
private String requestId; // 唯一标识
private String applicantId; // 申请人
private String status; // 状态:PENDING, APPROVED, REJECTED
private LocalDateTime createdAt;
public void approve() {
// 业务逻辑:只有待审批状态才能通过
if (!"PENDING".equals(this.status)) {
throw new IllegalStateException("只有待审批的单据才能通过");
}
this.status = "APPROVED";
}
}
4.2.2 值对象(Value Object)
值对象用于描述事物的属性,它没有唯一标识,通过属性值来定义相等性。值对象是不可变的,一旦创建就不能修改;如果需要不同的值,就创建一个新的值对象。
- 无标识: 两个值对象如果所有属性都相同,则视为同一个对象。
- 不可变性: 任何“修改”操作都应返回一个新实例。
- 示例: 地址(Address)、金额(Money)、审批节点配置(NodeConfig)。
// 值对象示例:审批节点配置
public class NodeConfig {
private final String nodeName; // 节点名称
private final int order; // 排序
private final String approverType; // 审批人类型:ROLE, USER, MANAGER
// 构造时赋值,不提供 setter 方法
public NodeConfig(String nodeName, int order, String approverType) {
this.nodeName = nodeName;
this.order = order;
this.approverType = approverType;
}
// 相等性基于所有属性
@Override
public boolean equals(Object o) {
// ... 实现基于属性的 equals 和 hashCode
}
}
4.2.3 聚合(Aggregate)
聚合是一组相关对象的集合,它被当作一个整体来管理数据一致性。聚合有一个根实体(聚合根),外部对象只能通过聚合根来访问聚合内部的其他实体或值对象。聚合是数据修改的最小单元。
| 概念 | 说明 | 审批流示例 |
|---|---|---|
| 聚合根 | 聚合的唯一入口,负责维护内部不变量 | ApprovalFlow(审批流模板) |
| 内部实体 | 属于聚合,但通过聚合根访问 | ApprovalNode(审批节点) |
| 值对象 | 聚合内部的属性描述 | NodeConfig(节点配置) |
| 业务规则 | 聚合根的方法确保数据一致性 | 不允许删除正在被引用的节点 |
下图展示了战术设计中实体、值对象与聚合的协作关系:
4.3 战略与战术的衔接:从业务到代码
战略设计回答“系统边界在哪”,战术设计回答“边界内如何建模”。两者缺一不可。在审批流参数配置优化场景中,衔接过程如下:
- 战略层: 将“审批流管理”识别为一个独立的限界上下文,与“业务单据上下文”分离。
- 战术层: 在审批上下文中,定义
ApprovalFlow为聚合根,ApprovalNode为内部实体,NodeConfig为值对象。 - 参数配置: 审批流的参数(如超时时间、审批人策略)封装在
NodeConfig值对象中,确保配置的不可变性和可替换性。 - 边界通信: 业务上下文通过领域事件或接口调用,将审批请求发送给审批上下文,而非直接操作审批内部模型。