《领域驱动设计》读书笔记:聚合根与实体
Eric Evans 的《领域驱动设计》(Domain-Driven Design)出版于 2003 年,到现在已经 20 多年。但每次重读这本书,都能从中学到新的东西。这一次我想专门聊聊"聚合根"这个概念——它是我在复杂业务系统里最常用、也最容易用错的设计工具。
什么是聚合根
聚合(Aggregate)是一组紧密相关的领域对象的集合,它们被当作一个数据修改单元。聚合根(Aggregate Root)是这个集合对外的唯一入口——外部对象只能通过聚合根来访问聚合内部的对象。
一个简单的例子:电商系统里的"订单"。订单本身是聚合根,订单里的多个订单项(OrderItem)、配送地址(ShippingAddress)、支付记录(Payment)都是聚合内部的实体。外部对象要修改订单的任何部分,都必须通过订单这个根,而不是直接改 OrderItem。
为什么要这样设计
聚合根的核心目的是保证数据修改的一致性边界。
假设没有聚合根的概念,我们直接改 OrderItem 的状态:
- 用户 A 改了一项订单项的数量
- 用户 B 同时改了同一个订单项的配送地址
- 两个修改没有冲突检测,最终产生一条"自相矛盾"的订单项
如果引入聚合根,所有修改都必须经过 Order 这个根。当 A 要修改订单项时,需要先加载整个 Order 聚合,加锁(乐观锁或悲观锁),修改后整体持久化。这样并发修改就被检测出来。
聚合的边界
Evans 在书里强调:聚合边界的设计是 DDD 中最重要的判断。
聚合太大,所有操作都加锁,并发性能差。聚合太小,一致性约束无法保证。
判断聚合边界的方法:找出"必须一起修改"的数据。
举几个例子:
- 订单 + 订单项:必须一起(订单项数量变了,订单总额必须变)
- 订单 + 用户:不必一起(用户信息变了不影响订单)
- 账户 + 账户流水:必须一起(账户余额 = 所有流水之和)
实体的"身份"
DDD 里有个关键概念:实体(Entity)vs 值对象(Value Object)。
- 实体:有唯一标识,身份比属性重要。比如"用户"——即使姓名改了,他还是同一个人
- 值对象:没有标识,靠属性定义。比如"地址"——两个地址完全相同就是同一个地址
在订单例子里:
- Order 是实体(每个订单有唯一 ID)
- OrderItem 是实体(在 Order 内部有自己的 ID)
- ShippingAddress 是值对象(换地址就换新对象)
我踩过的坑
第一次接触 DDD 时,我以为"聚合根就是把相关的实体包成一个类"。实际上不是。聚合根的本质是一致性边界,不是代码组织。
举个例子:把"用户-订单-支付"打包成一个聚合根是错的。用户和订单是独立的生命周期,不能因为代码上挨着就绑一起。
正确的做法:
- 用户聚合:包含用户基本信息、收货地址列表
- 订单聚合:包含订单、订单项、配送地址
- 支付聚合:包含支付记录、退款记录
三个聚合通过 ID 引用,不直接持有彼此对象。
工厂与仓储
聚合根还带来两个相关概念:
- 工厂(Factory):负责创建复杂的聚合。比如"创建订单"可能需要:检查库存、生成订单号、初始化订单项、记录审计日志——这整个流程应该封装在工厂里
- 仓储(Repository):每个聚合根有一个仓储,负责持久化和加载。外部对象不能直接通过数据库访问聚合,必须通过仓储
持久化的细节
聚合根改变了我们对 ORM 的使用方式。
错误的做法:用 ORM 加载整个数据库图,跨聚合根直接互相引用,最后数据修改一团乱。
正确的做法:每个聚合根独立持久化,加载时只加载该聚合内的对象。聚合之间通过 ID 引用,需要时显式加载对方聚合。
这种设计带来的好处:
- 性能更好:不会因为"想加载一个订单"而把整个数据库拉进内存
- 一致性更好:每个聚合是独立的数据修改单元
- 测试更容易:聚合可以独立测试,不需要复杂的 mock
一个反模式:贫血模型
DDD 警告我们不要把领域对象变成"只有 getter/setter 的空壳"——这叫贫血领域模型。
贫血模型的特征:
- 实体只有属性,没有业务行为
- 业务逻辑全部在 Service 层
- 实体只是数据传输对象
正确的做法是把业务行为放在领域对象里。比如 Order 不应该暴露 setStatus 方法,而应该有 pay()、cancel()、ship() 等行为,每个行为内部维护自己的不变性约束。
给我最大的启发
重读 DDD 这一章,我最大的收获是:一致性边界是设计的核心。
很多系统后期维护困难、性能低下,根本原因是"边界模糊"——数据之间互相纠缠,改一处影响十处。
聚合根的设计强迫我们思考:
- 哪些数据必须一起改?(决定聚合边界)
- 哪些数据可以独立改?(决定聚合之间用 ID 引用)
- 哪些操作必须原子?(决定一致性约束)
这些问题是任何复杂业务系统都需要回答的。DDD 提供了一个答案,但答案的细节需要你自己根据业务去找。
补充:微信读书里我划过的句子
获得的领域模型往往只有数据没有行为,Martin Fowler 将这样的对象组成的模型称为贫血模型。
在进行模型驱动设计时,若以数据库建立的模型作为设计的驱动力,就会很自然地得到贫血模型。
避免设计出只有数据属性的贫血模型。
但在建立领域模型时,结构建模范式提倡数据结构与算法分离的做法,会影响领域对象的封装能力。
这几段都来自张逸的《解构领域驱动设计》(注:不是 Eric Evans 原书,但作为中文 DDD 实践反思相当好)。张逸对贫血模型的剖析比我读 Evans 时更直白——数据库驱动的设计必然导致贫血模型,因为 SQL 表天然就是"数据 + 行为分离"的。
我读完这段后回头检查自己的代码,确实大量 Service 类是"贫血业务逻辑"——只在服务层有行为,领域对象只是 getter/setter。这是 DDD 实践最常见的退化模式。
另一段让我反复回味的:
抽象不应该依赖于细节,细节应该依赖于抽象。
谁更稳定?抽象更稳定。
这两句其实是 SOLID 中 DIP(依赖反转原则)的另一种表述。但张逸把"稳定"这个维度加进来,让原则变得更可操作——判断依赖方向的简单标准:更稳定的应该被依赖。
具体做法是"面向接口设计"——高层定义接口(更稳定),低层实现接口(变化更多)。这种设计让系统对变化更友好。
我认为需要抓住职责与抽象这两个核心。
这是张逸对 DDD 全书的提炼:领域对象的核心是"职责"(做什么)和"抽象"(如何被看待)。一个好的领域模型,职责清晰、抽象稳定;一个差的领域模型,职责模糊、抽象漂移。
推荐阅读
- Eric Evans《领域驱动设计》—— 原始经典
- Vaughn Vernon《实现领域驱动设计》(Implementing DDD)—— 更现代的实践指南
- Vaughn Vernon《领域驱动设计精粹》(DDD Distilled)—— 简明版入门
DDD 是一本厚书,但核心思想其实不复杂。重读它,每次都能从不同角度获得启发。