战略设计决定了系统的骨架,战术设计则决定了骨架内部的肌肉。这篇笔记用尽量通俗的方式,讲清楚战术设计中几个最核心的概念。
实体(Entity):有身份的”东西”
实体是有唯一标识、且会变化的对象。
类比:一个人。哪怕他改了名、换了工作、搬了家,他还是”这个人”——因为他的身份证号不变。
对应到代码,实体的关键是:
- 有唯一标识(ID);
- 有生命周期,状态会随业务变化;
- 身份稳定,ID 是判断”是不是同一个”的标准。
比如订单、用户、账户,都是典型的实体。两个订单即使内容完全一样,也是两个不同的对象,因为它们的 ID 不同。
值对象(Value Object):只描述”值”的东西
值对象是没有独立身份、完全由属性值决定的对象。
类比:一个地址。北京市朝阳区某某路 1 号——这就是它本身,你不需要给这个地址一个”ID”。换了个地址,就是换了个新对象。
值对象的特征:
- 没有身份,两个内容相同的值对象就是”相等”的;
- 不可变,值对象创建后不应被修改;
- 常用来封装一些概念,如地址、金额、坐标、时间段。
比如”金额”可以做成值对象,包含 金额数值 和 币种,而不是裸用一个 double。这样能避免”不同币种乱相加”这类低级错误。
实体 vs 值对象
判断一个对象该做成实体还是值对象,就看它有没有独立身份、是否可共享复用:
| 判断维度 | 实体 | 值对象 |
|---|---|---|
| 是否有 ID | 有 | 没有 |
| 是否可变 | 会变 | 不可变 |
| 是否被共享 | 独立拥有 | 可被共享引用 |
| 例子 | 订单、用户 | 金额、地址 |
聚合(Aggregate):一组对象的边界
聚合是 DDD 战术设计里最重要的概念。它把一组关联紧密的对象绑定在一起,由一个聚合根统一对外提供访问。
类比:一辆汽车。发动机、变速箱、轮子是它的部件,但你要启动汽车,只需要操作”启动按钮”(聚合根),不需要直接去操作每一个零件。
聚合的三个要点:
- 一个聚合根:对外唯一的入口,只有通过聚合根才能修改聚合内部的对象;
- 一致性边界:聚合内部的数据变更必须保持业务上的一致性;
- 数据访问最小化:外部不能绕过聚合根直接操作内部对象。
为什么需要聚合?
聚合是为了解决事务一致性和并发控制的复杂度。如果不设聚合,所有对象之间都能随意互相引用、互相修改,数据一致性就完全失控了。
聚合有一个重要设计原则:优先用小聚合。聚合越小,事务范围越小,并发冲突越少,系统扩展性越好。不要贪大。
仓储(Repository):数据访问的抽象
仓储负责在领域模型和持久化存储之间做桥接。它让领域层不需要关心数据到底存的是数据库、还是 Redis、还是文件。
类比:仓储就像书架。书架帮你把书放好、按需取回,但你不用关心书是放在书架的哪一格。
仓储的要点:
- 面向聚合根设计,一个聚合对应一个仓储;
- 只暴露领域需要的接口(如
findById、save),把数据库细节藏在实现里; - 让上层可以方便地替换持久化方案。
领域服务(Domain Service):放不下实体里的业务逻辑
有些业务逻辑不属于某一个实体,而是跨多个实体/聚合协作。这时就用领域服务来承载。
类比:一次转账。转账涉及转出账户和转入账户两个对象,它既不属于转出账户,也不属于转入账户,于是把它放在一个”转账服务”里。
判断是不是领域服务的标准:这个操作是否不适合放在任何一个实体/值对象里。如果可以放,就不要硬造服务,否则会退化成”贫血模型”。
战术设计的小总结
战术设计提供了一套建模工具箱:
| 概念 | 作用 |
|---|---|
| 实体 | 有身份的、会变化的核心对象 |
| 值对象 | 无身份、不可变、按值相等的对象 |
| 聚合 | 由聚合根统领的一致性边界 |
| 仓储 | 聚合与持久化的桥接 |
| 领域服务 | 承载跨对象的业务逻辑 |
这几个概念组合起来,就能把某个限界上下文内部建模得清晰、内聚。下一篇笔记,我会用一个实际的业务例子,把这些概念完整串起来,看看 DDD 到底怎么落地。