欢迎光临
我们一直在努力

DDD如何保护领域模型:聚合、聚合根、仓库与工厂的协同防御

一、引言

在前几篇文章中,我们逐步构建起了DDD战术设计的核心武器库:

  • 实体和值对象构成了领域模型的基本细胞;

  • 领域服务协调跨实体的业务逻辑;

  • 防腐层守护着领域边界,抵御外部概念的污染。

然而,这些细胞不能孤立存在。一个订单需要关联客户、商户、商品列表、收货地址;一个用户需要聚合账户信息、配送地址、角色权限。如何管理这些对象之间的复杂关系?如何保证跨多个对象业务操作的数据一致性?如何让外部调用者不必关心对象图的内部结构?

这正是聚合(Aggregate)、聚合根(Aggregate Root)、仓库(Repository)和工厂(Factory)要回答的问题。
它们是保护领域模型的最后一道,也是最关键的一道防线。


二、聚合:整体与部分的统一体

1. 从个体到系统

实体和值对象体现的是个体的能力——账户可以存钱、取钱;订单项包含商品和数量。
聚合体现的是这些个体的系统工作能力——一个订单聚合协调着客户、商户、商品列表、收货地址,共同完成下单、支付、发货的完整流程。

两个典型聚合示例:

java

// 订单聚合
public class Order {
private Account client;
private Account merchant;
private List<Product> products;
private Address address;
// …
}

// 用户聚合
public class User {
private String accountId;
private String username;
private String password;
private List<Address> allAddress;
private List<Role> roles;
// …
}

2. 聚合的核心作用:保证数据一致性

聚合最核心的职责是在实现共同的业务逻辑时,保证数据的一致性。

  • 当用户修改默认收货地址时,User 聚合必须确保 allAddress 列表中只有一个地址被标记为默认;

  • 当订单状态变更为“已支付”时,Order 聚合需要同时更新订单状态、支付记录、库存锁定状态。

这种一致性不是数据库层面的ACID事务,而是领域模型内部的不变规则(Invariants)。 聚合通过将相关对象封装在一个边界内,确保所有变更都通过聚合根进行,从而本地保证一致性,极大降低了分布式事务的复杂度。

3. 如何识别聚合?——整体与部分的关系

最实用的判断方法:

判断聚合关系最有效的方式,就是去探讨下是否符合整体与部分的关系。

  • 订单与订单项:整体与部分 ✅

  • 用户与收货地址:整体与部分 ✅

  • 商品与商品类目:整体与部分 ❌(商品可以独立存在,类目更多是一种分类标签)

如果某个对象离开了整体就没有独立存在的业务意义,它大概率应该是整体的一部分——这既是聚合的划分原则,也是对象导向设计中“组合优于继承”的体现。


三、聚合根:聚合的对外门户

1. 为什么需要聚合根?

如果外部应用可以直接访问聚合内部的任意对象,那么:

  • 聚合的封装性荡然无存;

  • 外部代码可能绕过业务规则直接修改内部状态;

  • 聚合内的一致性规则无人保障。

解决方案:每个聚合必须指定一个唯一的实体作为聚合根(Aggregate Root)。
聚合根是外部访问聚合的唯一入口,所有对聚合内部对象的操作都必须通过聚合根进行。

2. 聚合根的选择原则

  • 聚合根必须是实体(有唯一标识);

  • 聚合根通常是对外暴露的主要业务对象(如 Order、User);

  • 聚合根负责创建、存储、删除聚合内的其他对象;

  • 外部系统只能持有聚合根的ID,不能持有内部对象的引用。

文档中强调:

每个聚合中应确定唯一的聚合根实体。

在订单聚合中,Order 就是聚合根;在用户聚合中,User 是聚合根。外部服务只需知道订单ID或用户ID,即可通过仓库获取整个聚合,然后通过聚合根暴露的方法执行业务操作。

3. 聚合与聚合根的价值:极大简化对象关系图

没有聚合时,系统内的对象关系图是一张巨大的网状结构——订单关联用户,用户关联角色,角色关联权限……任何变更都可能引发雪崩式的影响范围分析。

有了聚合与聚合根,网状关系被分解为一组以聚合根为中心的星状结构。外部系统只需与聚合根打交道,内部对象的生命周期完全由聚合根管理。整个系统的复杂度被降解到聚合内部,而聚合之间仅通过聚合根ID进行引用。

这是DDD应对复杂业务最重要的战术手段。


四、仓库与工厂:聚合的生命周期管理

1. 工厂:封装复杂创建逻辑

聚合内部往往包含多个对象,它们之间存在复杂的关联关系和不变量约束。客户端直接 new 出这些对象并组装,极易遗漏关键步骤,破坏一致性。

工厂(Factory)负责封装聚合的创建过程,确保创建出的聚合始终处于有效状态。

  • 简单聚合可直接在聚合根中提供静态工厂方法;

  • 复杂聚合可抽取独立的 Factory 类。

工厂将“如何使用聚合”与“如何构建聚合”分离,客户端无需关心聚合内部的装配细节。

2. 仓库:提供聚合的检索与持久化

仓库(Repository)是聚合在存储端的映射。它为领域层提供了如同集合(Collection)一样的接口,用于获取或保存聚合根。

示意图:

text

仓库
Order Product Address … ← 领域层只依赖Repository接口
↑ ↑ ↑
└────────┴───────┘

DB/Redis ← 基础设施层实现

仓库的核心价值是隔离领域层与数据访问技术:

  • 领域层只依赖 Repository 接口;

  • 具体的实现(JPA、MyBatis、Redis、甚至文件系统)被封装在基础设施层;

  • 当存储技术变更时,领域层纹丝不动。

3. 仓库与工厂的分工

工厂(Factory)仓库(Repository)
职责 创建新的聚合实例 检索/持久化已有聚合
输入 原始数据、参数 聚合根ID、规格
输出 聚合根 聚合根
状态 瞬时 持久化

二者共同覆盖了聚合的完整生命周期:工厂负责“生”,仓库负责“活”与“存”。


五、聚合、仓库、工厂如何协同保护领域模型?

文档的小结给出了明确答案:

聚合、仓库、工厂这些概念的实际意义——保护领域模型。

这种保护体现在三个层面:

1. 封装与一致性(聚合)

聚合将相关对象封装在边界内,通过聚合根统一对外提供服务。内部状态不会暴露给外部,一致性规则在聚合方法内集中实施——这是对领域模型静态结构的保护。

2. 创建完整性(工厂)

工厂确保聚合在诞生之初就是完整、合法的。不完整的聚合无法被创建——这是对领域模型生命周期的保护。

3. 持久化隔离(仓库)

仓库将领域模型与数据库、缓存等技术实现彻底解耦。业务代码不再出现 EntityManager、JdbcTemplate、RedisTemplate——这是对领域模型纯洁性的保护。

三者结合,构成了一道从创建、运行到持久化的完整防御链。


六、小结

文档原汁原味的小结:

  • 聚合与聚合根设计的作用

    • 将复杂的对象关系分解为以聚合根为中心的星状结构;

    • 保证聚合内数据的一致性;

    • 极大简化系统整体关系图。

  • 通过仓库和工厂实现聚合的设计

    • 工厂负责聚合的创建,保证创建过程的完整性;

    • 仓库负责聚合的检索与持久化,隔离基础设施变化。

  • 聚合、仓库、工厂的实际意义——保护领域模型

    • 静态结构:聚合封装内部对象;

    • 动态行为:聚合根统一对外;

    • 生命周期:工厂创建,仓库存取;

    • 最终目标:无论技术如何变化,领域模型始终稳定、可演进。


  • 七、思考与拓展

    1. 实体与值对象的关联,与聚合的关联有什么不同?如何区分?

    • 实体与值对象的关联是细粒度、非独立的——值对象属于实体,没有独立生命周期;

    • 聚合内部实体之间是整体与部分的关系——部分的生命周期完全由整体管理;

    • 聚合之间的关联是通过ID引用的弱关联——彼此不持有对方的引用,不保证强一致性。

    区分的关键:
    是否构成整体与部分?部分离开整体是否还有业务意义? 如果是,则是聚合关系;否则可能是实体-值对象关系,或独立的聚合间引用。

    2. 仓库和工厂并不是DDD独有的,你的项目中,这一层是如何设计的?

    • 很多项目中的 DAO/Manager 其实扮演了仓库的角色,但通常接口暴露了太多技术细节(如 save(Entity) 直接对应数据库 insert/update);

    • DDD 仓库的接口应该完全使用领域语言:findByUserId(UserId userId) 而非 selectByCondition(Map params);

    • 工厂往往被省略,直接在服务层使用 new + setter——这正是聚合不完整、业务规则泄露的源头。

    3. 仓库与缓存如何有效结合?如何设计高效的缓存一致性仓库实现?

    这是一个极具工程价值的问题:

    • 缓存位置:仓库实现内部可封装多级缓存(本地缓存 + Redis);

    • 缓存粒度:以聚合根ID为键,缓存整个聚合的序列化快照;

    • 缓存一致性:

      • 只读查询:先读缓存,未命中则加载DB并回填;

      • 写操作:先更新DB,再删除缓存(避免更新缓存带来的并发一致性问题);

      • 最终一致性:可容忍短暂不一致的场景,使用 @Cacheable 配合过期时间;

    • 进阶方案:基于领域事件的缓存失效——聚合状态变更时发布事件,监听器清除相关缓存。


    八、结语

    聚合、聚合根、仓库、工厂——这四个概念在DDD战术设计中紧密耦合,却又各司其职。它们共同回答了三个核心问题:

    • 领域模型应该长成什么样子?(聚合与聚合根)

    • 领域模型如何诞生?(工厂)

    • 领域模型如何被找到和保存?(仓库)

    如果说实体和值对象是领域模型的“血肉”,领域服务是“肌肉”,防腐层是“免疫系统”,那么聚合、仓库、工厂就是领域模型的“骨架”——它们支撑起整个模型的结构,让模型在面对复杂业务和技术变迁时依然挺拔。

    赞(0)
    未经允许不得转载:171主机测评 » DDD如何保护领域模型:聚合、聚合根、仓库与工厂的协同防御
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址