2. 领域驱动设计基础:通用语言、限界上下文、实体与值对象

领域驱动设计(DDD)的核心在于通过模型驱动的方式,将复杂的业务逻辑转化为可维护、可演进的软件系统。本章聚焦于DDD的三大基石:通用语言、限界上下文以及实体与值对象。理解这三者是进行有效领域建模、划定模块边界以及制定接口版本迁移策略的前提。

2.1 通用语言(Ubiquitous Language)

通用语言是DDD中最基础也最容易被忽视的原则。它要求开发团队、领域专家、产品经理、测试人员等所有项目参与者,使用一套统一、精确、无歧义的术语进行沟通。这套术语直接来源于领域模型,并体现在代码、文档、会议、用户故事等所有交流载体中。

核心价值:消除“翻译层”。传统项目中,业务人员说“客户取消订单”,开发人员理解成“将订单状态字段置为‘已取消’并释放库存”。通用语言确保双方对“取消订单”这一行为及其背后的业务规则(如是否允许部分取消、取消后积分如何处理)有完全一致的理解。

通用语言的构建原则

  • 源于模型,反哺模型:术语必须来自领域模型,同时模型中的概念必须用通用语言命名。
  • 持续精炼:随着对业务理解的加深,通用语言需要迭代更新,并同步修改代码和文档。
  • 拒绝技术术语:避免使用“DAO”、“DTO”、“Service”等技术词汇作为通用语言的一部分。
  • 强制使用:在代码审查、需求评审、站会中,坚持使用通用语言,纠正偏离的表述。
场景 传统沟通(混乱) 通用语言(清晰)
需求讨论 “用户点那个按钮,后台把这个单子状态改了。” “客户提交‘订单支付’请求,系统将订单状态从‘待支付’变更为‘已支付’,并触发‘支付成功’领域事件。”
代码实现 order.setFlag(2); order.markAsPaid();
测试用例 “测试用例:验证状态变更。” “测试用例:验证‘已支付’订单不能再次支付。”

2.2 限界上下文(Bounded Context)

限界上下文是DDD中划分模块边界的核心模式。它定义了模型应用的边界。在一个限界上下文内部,通用语言和领域模型是完整且一致的;在不同的限界上下文之间,同一个概念(如“客户”)可能具有不同的含义和属性。

关键认知:限界上下文不是简单的“模块”或“微服务”,而是一个语义边界。它决定了哪些概念属于这个模型,哪些不属于。微服务通常是限界上下文的一种物理实现形式。

限界上下文与模块边界

在大型企业应用中,将整个业务拆分为多个限界上下文是应对复杂性的关键策略。每个上下文拥有独立的数据库、独立的部署单元和独立的演进节奏。接口版本迁移策略正是围绕这些边界展开的。

限界上下文与通用语言的关系 限界上下文 A:订单管理 通用语言:订单、支付、物流单 “客户” = 下单人 + 收货地址 实体:Order(订单) 值对象:Address(地址) 领域服务:PaymentService 限界上下文 B:客户关系管理 通用语言:客户、线索、合同 “客户” = 公司信息 + 联系人 实体:Customer(客户) 值对象:ContactInfo(联系方式) 领域服务:LeadScoring 上下文映射

2.3 实体与值对象

实体和值对象是领域模型中的两种基本构造块。正确区分它们,直接决定了模型的准确性和代码的可维护性。

实体(Entity)

实体是具有唯一标识且会持续变化的对象。即使两个实体的所有属性都相同,只要标识不同,它们就是不同的对象。实体的核心是它的身份,而非属性。

  • 特征:拥有ID(如订单号、用户ID);具有生命周期;状态可变。
  • 示例:一个“订单”实体,其状态会从“待支付”变为“已支付”,但其订单号始终不变。

值对象(Value Object)

值对象是描述事物属性的对象,它没有唯一标识。两个值对象如果所有属性都相同,则被视为同一个对象。值对象通常是不可变的。

  • 特征:无ID;不可变;通过属性值进行比较;可以共享。
  • 示例:“地址”是一个典型的值对象。如果两个订单的地址都是“北京市朝阳区...”,则它们被视为相同的地址值,无需为每个地址分配一个ID。
常见误区:将值对象设计为可变对象,或为值对象创建数据库主键。这会导致模型退化,丧失值对象带来的性能优化(如缓存共享)和语义清晰度。
维度 实体(Entity) 值对象(Value Object)
标识 有唯一标识(ID) 无标识,靠属性值定义
可变性 可变(状态可更新) 不可变(替换而非修改)
比较方式 通过ID比较 通过所有属性值比较
生命周期 有独立生命周期 依附于实体
数据库存储 通常有独立表 通常嵌入实体表或序列化
代码示例
public class Order {
    private OrderId id;
    private OrderStatus status;
    public void markAsPaid() {
        this.status = OrderStatus.PAID;
    }
}
public class Address {
    private final String street;
    private final String city;
    private final String zipCode;
    // 构造器、getter、equals/hashCode
}

2.4 三者协同:构建模型边界

通用语言、限界上下文、实体与值对象三者共同构成了DDD建模的骨架:

  1. 限界上下文划定了模型的物理和语义边界。
  2. 在边界内部,通用语言确保了沟通和代码的一致性。
  3. 实体与值对象作为模型的基本单元,在通用语言的指导下被精确地设计出来。

这种协同关系直接影响了后续的模块边界划分和接口版本迁移策略。例如,当两个限界上下文需要交互时,我们通过上下文映射(如防腐层、开放主机服务)来管理它们之间的依赖,而接口的版本迁移则必须考虑这些边界上的契约变化。

实体与值对象在限界上下文中的协作 限界上下文:订单管理 实体:Order 值对象:Money 值对象:Address 值对象:OrderLine 协作规则 1. 实体持有值对象的引用 2. 值对象不可变,替换即更新 3. 值对象可被多个实体共享 4. 实体通过ID追踪身份 5. 值对象通过属性值比较 6. 通用语言命名所有元素
实践建议:在开始编码前,先与领域专家一起,使用通用语言在限界上下文内识别出核心实体和值对象。一个简单的判断方法是:如果两个对象属性相同但需要区分,则它们是实体;如果属性相同就视为相同,则它们是值对象。