第10章 聚合
- 第1章 DDD入门
- 第2章 领域、子域和限界上下文
- 第3章 上下文映射图
- 第4章 架构
- 第5章 实体
- 第6章 值对象
- 第7章 领域服务
- 第8章 领域事件
- 第9章 模块
- 第10章 聚合
- 第11章 工厂
- 第12章 资源库
- 第13章 集成限界上下文
- 第14章 应用程序
本章学习路线图
- 以SaaSOvation为例,学习对聚合的不当建模所带来的负面影响
- 学习设计聚合的经验原则,并形成一套最佳实践
- 根据真实的业务规则,掌握如何在一致性边界中对真正的不变条件进行建模
- 学习一个聚合为什么应该通过标识(identity)去引用另一个聚合
- 了解在聚合边界之外使用最终一致性的重要性
- 学习聚合的实现技术,包括“告诉而非询问”原则和迪米特法则
将实体(5)和值对象(6)在一致性边界之内组成聚合。有很多途径都将导致我们建立不正确的聚合模型,我们应该将注意力集中在业务规则上来避免各种不良情况。
在Scrum核心领域中使用聚合
本章书中以SaaSOvation团队的ProjectOvation项目的敏捷项目管理上下文为例,通过Scrum项目建立模型的过程来给出了使用聚合的分析思路。
聚合模型讨论的是对象组合和信息隐藏,此外还包含了一致性边界和事务。
原则:在一致性边界之内建模真正的不变条件
要从限界上下文中发现聚合,我们需要了解模型中真正的不变条件。只有这样,我们才能决定什么样的对象可以放在一个聚合中。这里的不变条件表示一个业务规则,该规则应该总是保持一致性的。聚合边界之内的所有内容组成了一套不变的业务规则,任何操作都不能违背这些规则。边界之外的任何东西与该聚合都是不相关的。
在讨论不变条件时,我们讨论的是事务一致性。在设计聚合时,我们主要关注的是聚合的一致性边界,而不是创建一个对象树。
在一个事务中只能修改一个聚合实例。
原则:设计小聚合
对于大聚合,即便我们可以保证事务的成功执行,它依然有可能限制系统的性能和可伸缩性。系统性能和可伸缩性虽然是非功能性需求,但是我们绝不应该予以忽视。
属于DFX特性,具体详见《何所谓DFX-云社区-华为云》
如果我们要设计小的聚合,好的做法是使用根实体(Root Entity)来表示聚合,其中只包含最小数量的属性或者值类型属性。
这里的值类型属性即引用了值对象的属性,这里主要是为了与那些原始类型区分开来。
哪些属性是所需的呢?简单的答案是:那些必须与其他属性保持一致的属性。比如一个Product拥有name和desc属性,这里的name和desc是需要保持一致的,将它们放在两个不同的聚合中显然是没有意义的。
将聚合的内部建模成值对象有很多好处,根据你所选用的持久化机制,值对象可以随着根实体而序列化,而实体则需要单独的存储区域予以跟踪。再者由于值对象是不变的,测试起来也相对简单。
小聚合不仅有性能和可伸缩性上的好处,它还有助于事务的成果执行,即它可以减少事务提交冲突。
原则:通过唯一标识引用其他聚合
我们应该优先考虑通过全局唯一标识来引用外部聚合,而不是通过直接的对象引用。
通过这种方式创建的聚合也会变得更小,模型的性能也将随之变好。
建模对象导航性
在调用聚合行为方法之前,使用资源库或**领域服务(7)**来获取所需要的对象。不管使用哪种方式在一个聚合中引用另外的聚合,我们都不能在同一个事务中修改多个聚合实例。
原则:在边界之外使用最终一致性
任何跨聚合的业务规则都不能总是保持处于最新状态。通过事件处理、批处理或者其他更新机制,我们可以在一定时间之内处理好他方依赖。因此当在一个聚合上执行命令方法时,如果还需要在其他的聚合上执行额外的业务规则,那么请使用最终一致性。
DDD有一种很实用的方法可以支持最终一致性,即一个聚合的命令方法所发布的领域事件及时地发送给异步的订阅方。
DomainEventPublisher()
.instance()
.publish(event);
对于一致性的选择,有一个简单而使用的指导原则。对于一个用例,问问是否应该由执行该用例的用户来保证数据的一致性。如果是,请使用事务一致性,当然此时依然需要遵循其他聚合原则。如果需要其他用户或者系统来保证数据一致性,请使用最终一致性。
打破原则的理由
又是肯呢个需要在单个事务中更新多个聚合实例,这么做的理由有:
- 方便用户界面
- 缺乏技术机制
- 全局事务
- 查询性能
我们将尽可能保证一致性,并且致力于创建高性能的、高可伸缩性的系统。
通过发现,深入理解
本节描述聚合原则如何影响SaaSovation团队设计他们的Scrum模型。分别从重新思考设计、估算聚合成本、常见用例场景和内存消耗方面进行说明。
实现
我们应该在考虑健壮性之外,也应该全面地考虑**实体(5)、聚合(6)、领域服务(8)、模块(9)、工厂(11)和资源库(12)**中聚合的实现。
创建具有唯一标识的根实体
将实体建模成聚合根。每个聚合根必须拥有一个全局的唯一标识,相关内容参考实体(5)。示例见本节内容。
public ProductId nextIdentity(){
return new ProductId(UUID.randomUUID().toString());
}
优先使用值对象
我们应该尽量地将根实体所包含的其他聚合建模成值对象,而不是实体。在不至于对模型或者基础设施造成明显影响的情况下,采用值对象全部替换的方式时最好的选择。
使用迪米特法则和“告诉而非询问”原则
这两个法则都强调信息隐藏。
- 迪米特法则: 强调了“最小知识原则”。在客户端对象使用服务对象时,它应该尽可能少的知道服务对象的内部结构。
- 告诉而非询问原则:一个对象不应该被告知如何执行操作。
客观并发
接下来,我们需要考虑如何防止乐观并发的版本号。在我们定义聚合时,最安全的方法便是只为根实体创建版本号。每次在聚合内部执行状态修改命令时,根实体的版本号都会随之增加。
避免依赖注入
对于所依赖的对象,我们应该在聚合命令方法执行之前进行查找,然后再将其传入命令方法。
以上只是建议大家不要再聚合中注入资源库和领域服务,其他多数情况下,依赖注入都是很合适的。