2. 领域驱动设计基础:通用语言、限界上下文、实体与值对象
领域驱动设计(DDD)的核心在于通过模型驱动的方式,将复杂的业务逻辑转化为可维护、可演进的软件系统。本章聚焦于DDD的三大基石:通用语言、限界上下文以及实体与值对象。理解这三者是进行有效领域建模、划定模块边界以及制定接口版本迁移策略的前提。
2.1 通用语言(Ubiquitous Language)
通用语言是DDD中最基础也最容易被忽视的原则。它要求开发团队、领域专家、产品经理、测试人员等所有项目参与者,使用一套统一、精确、无歧义的术语进行沟通。这套术语直接来源于领域模型,并体现在代码、文档、会议、用户故事等所有交流载体中。
核心价值:消除“翻译层”。传统项目中,业务人员说“客户取消订单”,开发人员理解成“将订单状态字段置为‘已取消’并释放库存”。通用语言确保双方对“取消订单”这一行为及其背后的业务规则(如是否允许部分取消、取消后积分如何处理)有完全一致的理解。
通用语言的构建原则
- 源于模型,反哺模型:术语必须来自领域模型,同时模型中的概念必须用通用语言命名。
- 持续精炼:随着对业务理解的加深,通用语言需要迭代更新,并同步修改代码和文档。
- 拒绝技术术语:避免使用“DAO”、“DTO”、“Service”等技术词汇作为通用语言的一部分。
- 强制使用:在代码审查、需求评审、站会中,坚持使用通用语言,纠正偏离的表述。
| 场景 | 传统沟通(混乱) | 通用语言(清晰) |
|---|---|---|
| 需求讨论 | “用户点那个按钮,后台把这个单子状态改了。” | “客户提交‘订单支付’请求,系统将订单状态从‘待支付’变更为‘已支付’,并触发‘支付成功’领域事件。” |
| 代码实现 | order.setFlag(2); |
order.markAsPaid(); |
| 测试用例 | “测试用例:验证状态变更。” | “测试用例:验证‘已支付’订单不能再次支付。” |
2.2 限界上下文(Bounded Context)
限界上下文是DDD中划分模块边界的核心模式。它定义了模型应用的边界。在一个限界上下文内部,通用语言和领域模型是完整且一致的;在不同的限界上下文之间,同一个概念(如“客户”)可能具有不同的含义和属性。
关键认知:限界上下文不是简单的“模块”或“微服务”,而是一个语义边界。它决定了哪些概念属于这个模型,哪些不属于。微服务通常是限界上下文的一种物理实现形式。
限界上下文与模块边界
在大型企业应用中,将整个业务拆分为多个限界上下文是应对复杂性的关键策略。每个上下文拥有独立的数据库、独立的部署单元和独立的演进节奏。接口版本迁移策略正是围绕这些边界展开的。
2.3 实体与值对象
实体和值对象是领域模型中的两种基本构造块。正确区分它们,直接决定了模型的准确性和代码的可维护性。
实体(Entity)
实体是具有唯一标识且会持续变化的对象。即使两个实体的所有属性都相同,只要标识不同,它们就是不同的对象。实体的核心是它的身份,而非属性。
- 特征:拥有ID(如订单号、用户ID);具有生命周期;状态可变。
- 示例:一个“订单”实体,其状态会从“待支付”变为“已支付”,但其订单号始终不变。
值对象(Value Object)
值对象是描述事物属性的对象,它没有唯一标识。两个值对象如果所有属性都相同,则被视为同一个对象。值对象通常是不可变的。
- 特征:无ID;不可变;通过属性值进行比较;可以共享。
- 示例:“地址”是一个典型的值对象。如果两个订单的地址都是“北京市朝阳区...”,则它们被视为相同的地址值,无需为每个地址分配一个ID。
常见误区:将值对象设计为可变对象,或为值对象创建数据库主键。这会导致模型退化,丧失值对象带来的性能优化(如缓存共享)和语义清晰度。
| 维度 | 实体(Entity) | 值对象(Value Object) |
|---|---|---|
| 标识 | 有唯一标识(ID) | 无标识,靠属性值定义 |
| 可变性 | 可变(状态可更新) | 不可变(替换而非修改) |
| 比较方式 | 通过ID比较 | 通过所有属性值比较 |
| 生命周期 | 有独立生命周期 | 依附于实体 |
| 数据库存储 | 通常有独立表 | 通常嵌入实体表或序列化 |
| 代码示例 | |
|
2.4 三者协同:构建模型边界
通用语言、限界上下文、实体与值对象三者共同构成了DDD建模的骨架:
- 限界上下文划定了模型的物理和语义边界。
- 在边界内部,通用语言确保了沟通和代码的一致性。
- 实体与值对象作为模型的基本单元,在通用语言的指导下被精确地设计出来。
这种协同关系直接影响了后续的模块边界划分和接口版本迁移策略。例如,当两个限界上下文需要交互时,我们通过上下文映射(如防腐层、开放主机服务)来管理它们之间的依赖,而接口的版本迁移则必须考虑这些边界上的契约变化。
实践建议:在开始编码前,先与领域专家一起,使用通用语言在限界上下文内识别出核心实体和值对象。一个简单的判断方法是:如果两个对象属性相同但需要区分,则它们是实体;如果属性相同就视为相同,则它们是值对象。