4. 领域驱动设计(DDD)入门

在复杂的企业业务系统中,模块边界模糊、业务逻辑散落是导致审批流配置混乱的根本原因。领域驱动设计(DDD)提供了一套从业务出发的建模方法,帮助我们在“业务复杂度”与“技术实现”之间建立清晰的映射。本章聚焦 DDD 的两大核心阶段:战略设计(划定边界)与战术设计(构造模型),为后续的模块拆分与参数优化奠定理论基础。

4.1 战略设计:划定业务边界与统一语言

战略设计的核心目标是解决“系统该拆成几块”以及“团队之间如何沟通”的问题。它不关心具体代码,而是关注业务领域的划分与协作。

4.1.1 限界上下文(Bounded Context)

限界上下文是 DDD 中最关键的边界概念。它定义了一个显式的边界,在边界内部,领域模型、术语、业务规则保持严格一致;边界之间通过明确的接口进行通信。每个限界上下文对应一个独立的业务子系统或微服务。

核心原则: 一个限界上下文内部,一个术语只有一个含义;不同上下文中,同一个词可能代表完全不同的概念。

例如,在电商系统中,“订单”在“销售上下文”中代表待支付的交易记录,而在“物流上下文”中则代表已打包的包裹。强行统一这两个“订单”会导致模型臃肿且难以维护。

限界上下文 核心职责 典型术语示例
销售上下文 处理客户下单、支付、促销 购物车、订单、优惠券
库存上下文 管理商品库存、出入库 SKU、库存量、仓库
物流上下文 安排配送、跟踪包裹 运单、配送站、签收
审批上下文 定义审批流程、处理审批任务 审批模板、审批节点、审批人
实战提示: 在拆分审批流模块时,应将其视为独立的“审批上下文”。不要将审批逻辑散落在订单、报销、采购等业务上下文中,否则会导致审批参数配置混乱,难以统一优化。

4.1.2 通用语言(Ubiquitous Language)

通用语言是团队(业务专家、产品、开发、测试)共同使用的业务语言。它必须基于限界上下文,确保在同一个上下文内,所有人对同一个词的理解完全一致。通用语言应直接体现在代码、文档、讨论和用户故事中。

  • 来源: 从业务专家的术语中提炼,而非技术术语。
  • 载体: 代码中的类名、方法名、变量名;需求文档;测试用例。
  • 维护: 当业务规则变化时,通用语言必须同步演进。

下图展示了战略设计中限界上下文与通用语言的关系:

战略设计:限界上下文与通用语言 销售上下文 通用语言:订单、支付 模型:Order, Payment 团队:销售产品组 库存上下文 通用语言:SKU、库存 模型:Product, Stock 团队:仓储产品组 审批上下文 通用语言:审批流、节点 模型:ApprovalFlow, Node 团队:BPM 产品组 关键原则 1. 每个上下文拥有独立的通用语言,术语含义仅在边界内有效。 2. 上下文之间通过“上下文映射”协作,如防腐层(ACL)、开放主机服务(OHS)。 3. 审批上下文作为支撑子域,为其他上下文提供统一的审批能力。

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(节点配置)
业务规则 聚合根的方法确保数据一致性 不允许删除正在被引用的节点
常见误区: 将数据库表的一对多关系直接映射为聚合关系。聚合的设计应基于业务一致性边界,而非数据存储结构。例如,审批流模板和审批节点在业务上必须同时加载、同时保存,因此应放在同一个聚合中。

下图展示了战术设计中实体、值对象与聚合的协作关系:

战术设计:聚合、实体与值对象 聚合边界:审批流模板(ApprovalFlow) 聚合根:ApprovalFlow 实体(有ID:flowId) ApprovalNode 实体(有ID:nodeId) NodeConfig 值对象(无ID,不可变) 聚合规则 • 外部对象只能通过 ApprovalFlow(聚合根)访问 ApprovalNode 和 NodeConfig。 • 删除 ApprovalFlow 时,其内部的 Node 和 Config 必须一起删除,保证数据一致性。

4.3 战略与战术的衔接:从业务到代码

战略设计回答“系统边界在哪”,战术设计回答“边界内如何建模”。两者缺一不可。在审批流参数配置优化场景中,衔接过程如下:

  1. 战略层: 将“审批流管理”识别为一个独立的限界上下文,与“业务单据上下文”分离。
  2. 战术层: 在审批上下文中,定义 ApprovalFlow 为聚合根,ApprovalNode 为内部实体,NodeConfig 为值对象。
  3. 参数配置: 审批流的参数(如超时时间、审批人策略)封装在 NodeConfig 值对象中,确保配置的不可变性和可替换性。
  4. 边界通信: 业务上下文通过领域事件或接口调用,将审批请求发送给审批上下文,而非直接操作审批内部模型。