领域驱动设计:从战略设计到战术实现
引言:为什么需要领域驱动设计?
在复杂的业务系统中,传统的以数据为中心的设计方式往往会导致业务逻辑散落在各处,代码难以维护,团队沟通成本剧增。领域驱动设计(Domain-Driven Design,DDD)提供了一套系统的方法,让开发团队专注于核心业务领域,通过通用语言、限界上下文、聚合等技术,构建出高内聚、可演化、业务表达清晰的软件模型。
本教程将从 DDD 的核心思想出发,带你完成战略设计与战术实现的全流程实战。
第一部分:核心思想与战略设计
1.1 通用语言(Ubiquitous Language)
DDD 的基石是建立团队成员(领域专家、产品、开发、测试)共同使用的一套语言,并直接映射到代码中的类名、方法名。
实战要点:
- 从领域专家那里挖掘术语,而不是自行创造。
- 所有需求文档、白板讨论、代码模型必须使用同一套词汇。
- 如果发现不同团队成员对同一术语理解不同,立刻召开“术语对齐会”,必要时引入新词或划分限界上下文。
1.2 限界上下文(Bounded Context)
一个大型系统不可能只用一个模型覆盖所有功能。限界上下文明确了某个模型在特定范围内的含义和职责。
如何识别限界上下文:
- 找出现有系统中的语言冲突:例如“订单”在交易上下文和物流上下文中,行为与属性完全不同。
- 按业务能力划分:如“用户管理”“商品管理”“支付”“物流”通常是不同的上下文。
- 每个上下文拥有独立的模型、数据库、团队,本质上是一个微服务候选项。
上下文映射(Context Map):
- 确定上下文之间的关系:共享内核、客户-供应商、防腐败层、开放主机服务等。
- 为每个集成点选择正确的模式,防止模型污染。
1.3 子域划分
将业务系统分解为核心域、支撑域、通用域,合理分配技术投入。
- 核心域: 公司真正的竞争力所在,需要投入最精心的设计。
- 支撑域: 辅助完成核心业务,但不产生差异化优势,可以适度标准化。
- 通用域: 现成的系统或外包即可,如认证授权、消息推送。
第二部分:战术设计——将模型落实到代码
2.1 实体(Entity)
拥有唯一标识、连续生命周期的对象。即使属性全部改变,标识也能保持不变。
建模原则:
- 标识生成策略:UUID、数据库自增ID、业务编号(如订单号)。
- 尽量保持一致性和不变条件检查在实体内部,避免“贫血模型”。
- 示例(Java风格伪代码):
public class Order {
private OrderId id;
private CustomerId customerId;
private Money total;
public void applyDiscount(Discount discount) {
// 业务规则
this.total = this.total.subtract(discount.amount());
}
}
2.2 值对象(Value Object)
没有唯一标识,通过属性值来定义的不可变对象。它们描述事物的特征,可以安全共享。
常见值对象: Money、Address、Email、日期范围。
好处:
- 自带业务行为,如货币相加时校验币种。
- 提升代码可读性和安全性。
- 实现值对象的相等性基于属性比较。
2.3 聚合(Aggregate)与聚合根(Aggregate Root)
聚合是一组相关对象的集合,对外通过聚合根来保证一致性边界。
黄金法则:
- 一次事务只能修改一个聚合实例,通过聚合根操作内部实体。
- 小聚合优于大聚合,设计时考虑最终一致性而非全局强一致。
- 根据业务不变量确定聚合边界:例如“订单总额必须等于订单项金额之和”这个不变量就需要 Order 作为聚合根,OrderItem 只能通过 Order 访问。
2.4 领域服务(Domain Service)
当某个业务行为无法自然归属于某个实体或值对象时,使用无状态的领域服务。
适用场景:
- 操作涉及多个聚合。
- 需要复杂的计算或本地外部系统交互。
- 命名必须出自通用语言,如:FundsTransferService。
2.5 资源库(Repository)
资源库封装了聚合的存储与检索逻辑,模拟为“内存中的集合”。
设计要点:
- 只对聚合根提供资源库,例如 OrderRepository。
- 接口定义在领域层,实现在基础层。
- 常用方法:save、load、findByCriteria。
2.6 领域事件(Domain Event)
描述领域中已经发生的有意义的事情,其他上下文可以订阅响应。
实践方式:
- 事件命名采用过去式,如 OrderPlaced、PaymentReceived。
- 在实体方法内部生成事件,通过事件发布器发送。
- 用于最终一致性、审计日志、解耦上下文。
2.7 模块与包结构
将领域模型组织为清晰的模块,尽量让限界上下文和包结构对应。
建议分层:
com.example.ordercontext
├── domain // 实体、值对象、领域服务、资源库接口
├── application // 应用服务、用例编排、DTO
├── infrastructure // 资源库实现、消息发送、外部接口适配
└── interfaces // REST API、消息监听器、定时任务
第三部分:实战流程与模式
3.1 业务需求分析到模型落地流程
- 事件风暴工作坊: 领域专家和开发团队一起梳理业务流程,用橙色便利贴表示事件,用蓝色表示命令,识别出聚合和上下文边界。
- 识别核心模型: 提取出实体、值对象、聚合,画出 UML 草图(可在白板上)。
- 编写模型代码: 先写领域层的测试,再写实现,遵循“测试先行”。
- 与基础设施对接: 实现资源库,重点关注业务逻辑与存储逻辑分离。
- 持续重构: 随着对领域理解加深,不断调整聚合边界和表达。
3.2 常见反模式与解决方案
- 贫血领域模型: 实体只有 getter/setter,业务逻辑全在 service 中。应把相关行为移到实体内部。
- 过度聚合: 一个聚合包含过多对象导致性能低下。使用最终一致性和小聚合,按事务边界拆分。
- ACID 思维僵化: 跨聚合强一致性会破坏独立性。善用领域事件和 Saga 实现最终一致。
- 无上下文映射: 上下文之间直接数据耦合,导致“大泥球”。必须通过防腐层或共享语言隔离变化。
3.3 与微服务架构的关系
DDD 的限界上下文是划分微服务的天然边界。一个上下文通常对应一个微服务(或多个微服务如果上下文内部进一步解耦)。战术设计中的聚合则成为服务内部的模块边界。遵循 DDD 设计出的微服务天然具有高内聚和独立演化的能力。
结语
领域驱动设计并非复杂的教条,而是一套将业务复杂性转化为可维护软件的有效方法论。从战略层面的通用语言和限界上下文,到战术层面的聚合、实体、值对象、事件,它的核心在于:让代码说出业务的语言,让模型抓住不变的本质。
建议你先在一个小型核心域中实践上述技术,逐步体会 DDD 带来的设计清晰度和长期价值。