十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

聚合(Aggregate)到底是什么?domain-driven-hexagon中的聚合边界设计详解

聚合(Aggregate)到底是什么?domain-driven-hexagon中的聚合边界设计详解 聚合Aggregate到底是什么domain-driven-hexagon中的聚合边界设计详解【免费下载链接】domain-driven-hexagonLearn Domain-Driven Design, software architecture, design patterns, best practices. Code examples included项目地址: https://gitcode.com/gh_mirrors/do/domain-driven-hexagon在领域驱动设计DDD中聚合Aggregate是新手最容易感到困惑的构建块之一它到底是一组实体的集合还是某种数据库表结构开源项目 domain-driven-hexagon 用 TypeScript NestJS 编写了大量可运行的 DDD 代码示例其中user和wallet两个模块非常适合作为学习聚合边界设计的样板间。本文将结合该项目源码用尽量少的代码讲清楚聚合是什么、聚合根为什么是唯一入口、聚合之间的边界该如何划分以及事务与领域事件如何保证聚合的一致性。一、先搞懂概念聚合到底聚合了什么一句话总结聚合是一组可以被视为整体来处理的领域对象的簇它把概念上必须保持一致的实体和值对象圈进同一个边界Consistency Boundary内。打个比方 银行里的账户是一个聚合根账户下的交易记录是内部实体。你对外操作时只需要找到账户本身交易记录通过账户访问——没有人会绕过账户直接改某一条交易。这个项目对聚合规则的定义非常完整可以对照 README.md 中的 Aggregates 章节核心规则有 6 条规则通俗解释聚合根拥有全局唯一身份用 UUID 作为主键全局可识别内部实体只有局部身份在聚合内唯一即可不需要对外暴露聚合根是唯一入口外部引用只能指向聚合根不能直接引用内部对象操作必须事务化要么全部保存成功要么全部回滚只能直接查出聚合根其他对象必须通过聚合根遍历获得聚合之间只通过 ID 引用避免持有直接的内存对象引用这些规则的共同目的只有一个用边界简化业务模型。边界内的对象必须始终一致任何一处变更提交后整个聚合的不变量都要满足边界外则通过 ID 和事件解耦。 项目里有一句总结很精准把多个相关实体和值对象放进同一个根 Entity这个根 Entity 就成了聚合根这个对象簇就成了聚合。二、聚合的三层积木Entity → AggregateRoot → ValueObjectdomain-driven-hexagon 把聚合相关的积木抽象在了公共库中先看继承关系自下而上Entityentity.base.ts 定义了实体的通用能力——id身份、基于 id 的相等判断、getProps()快照输出以及一个必须实现的抽象方法validate()不变量检查的落点。AggregateRootaggregate-root.base.ts 在实体基础上增加了领域事件箱内部可以addEvent()收集事件持久化成功后统一publishEvents()发布然后clearEvents()清空。ValueObject值对象address.value-object.ts 是典型例子——Address没有身份、不可变由 country / street / postalCode 三个字段构成创建时即校验长度合法。 值对象被聚合根当作属性使用在 user.entity.ts 中UserEntity直接持有一个Address值对象而不是三个散落的字符串字段——这正是聚合封装的体现业务规则地址长度必须合法跟着对象走而不是散落在各处的服务里。聚合根的最小实现以 UserEntity 为例export class UserEntity extends AggregateRootUserProps { static create(create: CreateUserProps): UserEntity { /* 生成UUID、挂上UserCreated领域事件 */ } updateAddress(props: UpdateUserAddressProps): void { /* 只允许改地址其余字段不可随意改 */ } validate(): void { /* 保存前由仓库调用保护不变量 */ } }可以看到三个聚合设计要点静态工厂create()避免空构造函数创建时即生成全局唯一 id 并挂上UserCreatedDomainEvent见 user-created.domain-event.ts方法而非 setter 修改状态updateAddress()只开放地址这一项注释里写得明白——This prevents from illegal actions防止外部直接user.email otherEmailvalidate()留白但必须存在它是聚合的守门员仓库在写库前一定会调用它。 对比一下 wallet.entity.tsWalletEntity的withdraw()会检查余额是否足够validate()则强制余额不能小于 0——这就是聚合必须保护自己的不变量的最小化示范。三、聚合边界设计为什么 User 和 Wallet 是两个聚合这是本项目的精髓。用户注册后应该自动拥有一个钱包直觉上你可能想把wallet字段直接塞进UserEntity里。但项目选择了两个独立聚合理由如下一致性要求不同User 的变更改地址、改角色和 Wallet 的变更存钱、取钱不需要在同一个事务里强一致生命周期独立钱包有自己完整的业务规则余额不能为负塞进用户聚合会让用户聚合过大保持聚合足够小README 明确建议 Try to avoid aggregates that are too big大聚合会带来性能和并发锁问题。边界内ID 引用 领域事件协作两个聚合之间的协作完全符合边界规则链路如下应用服务 create-user.service.ts 创建UserEntity并调用insert仓库写入数据库后UserEntity 挂着的UserCreatedDomainEvent被发布钱包模块的事件处理器 create-wallet-when-user-is-created.domain-event-handler.ts 订阅该事件创建WalletEntity注意WalletEntity只持有userId一个 ID不持有 User 对象引用——聚合之间通过全局 ID 引用避免直接对象引用。 关键点用户模块完全不知道钱包模块的存在依赖方向被领域事件反转了。这正是聚合边界带来的解耦红利——将来想把钱包拆成独立微服务只需把进程内事件换成消息队列领域逻辑一行不用改。四、一致性保障validate 事务 事件发布的三重奏聚合的事务性在 sql-repository.base.ts 这个通用仓库基类中落地它规定了每个聚合写库的固定动作写库前对每个聚合执行validate()不变量不满足直接抛错Fail Fast写库中执行 SQL 持久化主键冲突会转成ConflictException写库后调用publishEvents()发布领域事件并清空事件箱——保证事件只在数据真正落库后才对外可见。而跨聚合的原子性由事务兜底create-user.service.ts 中用userRepo.transaction(...)把创建用户 事件处理器创建钱包包进同一个数据库事务做到要么都成功要么都不发生。 这套机制把聚合三大铁律事务化操作、不变量保护、事件解耦全部收敛到了仓库层一处业务模块无需重复实现——这是分层架构下公共积木 具体模块协作的好例子。五、聚合边界设计清单照着这 7 条做就对了综合 domain-driven-hexagon 的实践给新手一份可直接套用的检查清单✅先问必须强一致吗只有需要在同一事务内保持一致的对象才放进一个聚合✅给聚合根选一个全局唯一 IDUUID 即可内部对象用局部 ID✅把所有写操作收口为聚合根的方法禁用公共 setter非法操作在类型/方法层面就无法表达✅值对象代替散落的原始字段如 Address把校验封装在对象内部✅聚合之间只存 ID不存对象需要联动就用领域事件✅保存前必须有validate()兜底把非法状态变成会抛错的异常而非静默数据✅聚合宁小勿大一个聚合一句话能说清一致性范围说不清就说明边界画歪了。六、写在最后回到标题的问题聚合不是一堆实体的袋子而是一套关于一致性的契约——边界内永远一致边界外只谈 ID 和事件。domain-driven-hexagon 用UserEntity和WalletEntity这两个小而完整的聚合把抽象概念落成了可运行的代码聚合根是入口、值对象是属性、validate()是守门员、领域事件是边界之间的外交官。想动手验证的话可以按 README.md 搭建环境后从 src/modules/user/ 和 src/modules/wallet/ 两个模块读起配合 tests/user/create-user.feature 中的行为测试观察创建用户 → 自动创建钱包这条完整的聚合协作链路。【免费下载链接】domain-driven-hexagonLearn Domain-Driven Design, software architecture, design patterns, best practices. Code examples included项目地址: https://gitcode.com/gh_mirrors/do/domain-driven-hexagon创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表