05 | 聚合和聚合根:怎样设计聚合?
05 | 聚合和聚合根:怎样设计聚合?
讲述:欧创新
时长13:21大小10.68M
聚合
聚合根
怎样设计聚合?
聚合的一些设计原则
总结
思考题
赞 38
提建议
精选留言(155)
- 南山置顶2019-10-23老师,麻烦有空帮忙看一下 场景:电销 根据任务类型的属性创建具体的定期执行任务,调度器把到点的任务放到执行器里去执行。执行完了等待下一次执行,对任务生成的明细可以填写沟通记录(第三方服务)但是本服务要提供此字段查询 过程中记录统计日志。每个任务类型在查询他的任务时查询和列表展示字段集合都不一样 任务执行过程:从数据源获取数据 -> 根据任务配置的过滤规则过滤 -> 生成任务明细(有过期时间) -> 1.自动分配给销售/2.销售主动去领单,过程中生成任务执行日志,统计日志 领域对象:任务类型、任务、任务明细、日志、过滤规则、字段、销售、 聚合A:聚合根-任务,任务明细-实体,值对象:日志、任务类型、销售 聚合B:聚合根-过滤规则, 聚合C:聚合根-字段,值对象,展示字段属性定义集合、查询字段属性定义集合 问题: 1.一个聚合中,允不允许只有一个实体和一些属性值? 2.是否合理?不合理该怎么设计呢? 3.任务是根据任务类型创建出来的,聚合A是不是不合理?或者实体的初始化可不可以依赖它的值对象呢? 4.任务类型决定它可以进行哪些过滤,实际创建任务时才会真正选择使用哪些过滤规则,过滤规则算什么呢?能作为一个独立的聚合吗? 5.字段也是同样的,并且字段是没有一个生命周期的,这种情况下是不是作为值对象更合理? 但是聚合A和B都用到了字段,值对象能在多个聚合公用吗?展开
作者回复: 我先理解一下你说的业务场景哈。不知道理解的对不对?不对的地方请你指出。由于不好展示事件风暴的过程,我就口述吧。 你描述的业务场景主要包括这两个部分吧。 流程一:创建任务 1、根据任务规则获取任务基础数据,生成任务。(产生任务已创建事件,领域对象包括:任务生成的基础数据、数据过滤规则、任务、任务类型) 2、自动将任务分配给销售。(销售的数据应该来源于其他系统,这个过程实际上是一个给任务赋值的过程,领域对象包括:销售) 3、销售领取任务。(给任务分配销售) 这个过程领域对象包括:基础数据、数据过滤规则、任务、任务类型、销售。 命令有:创建任务,给任务分配销售。 领域事件有:任务已创建。 流程二、任务执行 1、销售查询并获取任务,执行任务。(不清楚你说的字段在这个过程是什么含义,是查询时勾选类型吗) 2、任务执行完成后,记录执行结果,产生任务执行日志。(产生任务执行日志已创建事件,领域对象有:任务、任务日志) 3、统计日志。这一块不清楚你的业务逻辑和流程。 因为有统计日志,是不是就会去查询任务的执行日志,如果是这样的话,任务日志就需要设计为实体,跟任务关联,这里任务是聚合根。 这个阶段的领域对象有:任务和任务执行日志。 命令有:查询任务,生成任务执行日志。 领域事件有:执行任务日志已创建。 结合这两个流程,我们整体来分析一下。 领域对象包括:基础数据、数据过滤规则、任务、销售、任务类型、任务执行日志。 由于销售人员数据来源于第三方,以值的形式存储在任务中,因此我们可以将它设计为任务的值对象。 任务执行日志依附于任务,但是由于它后续要做查询和统计分析,因此将它设计为被任务引用的实体。 任务类型是任务的值对象 其它的基础数据、数据过滤规则这两个领域对象很独立,你可以理解他们是独立的实体,或者说一个实体就是一个聚合。 但是这样设计在代码目录设计时会显得比较单薄,一个聚合会有一个仓储和聚合自己的代码目录结构。因此我们可以将这两个实体可以直接放在任务的聚合里,但是他们的生命周期不受任务这个聚合根管理。 这样的话,我们就可以只建立一个任务聚合。这个聚合的聚合根是任务。它引用的实体包括任务执行日志,任务的值对象有:销售、任务类型。还有两个独立实体:基础数据和数据过滤规则。 说明一下: 在不少的数据统计和计算场景中,有很多实体之间相互独立,只参与计算和统计分析,但是这类场景中业务内聚性又很高,你找不出管理这些实体的聚合根。我称这种业务模型是非典型领域模型。虽然有些方面(比如聚合根)不符合DDD的一些原则,但是我们也可以按照DDD方法来完成设计。
共 6 条评论125 - 胖虎2019-10-31老师,能用电商的例子说一下聚合根的使用场景嘛
作者回复: 电商里面比较典型的几个聚合根,比如:库存、商品、订单等等。 以订单为例,订单在聚合里是聚合根,与订单关联的有订单明细和收货地址。订单明细包括商品ID,商品名称,价格以及数量等信息,由于订单明细是多个,它是一个集合,它被设计为实体,被订单引用。而订单只有一个收货地址,这个收货地址的值来源于你个人中心维护的收货地址,收货地址只能被整体替换,所以它被设计为值对象。
共 6 条评论37 - 渊虹2019-10-24老师,有个问题不明白,麻烦解惑。投保聚合和客户聚合中,投保人和被保人跨聚合引用到客户的id,需求是查询以客户s为被保人的保单。就需要跨过聚合根,直接访问被保人这个值对象。这个是不是和只能通过聚合根访问聚合内其他对象的理论不一致
作者回复: 被保人这个值对象以属性嵌入的方式嵌入保单聚合根中,查询客户保单时你不需要到客户聚合去查询客户信息了,直接根据客户信息在投保聚合查保单就可以了,当然这个客户信息不只是ID。 说明一下:这个跨聚合引用是在生成保单的时候,通过客户聚合根查询获取的客户信息,从客户聚合获取客户信息后,客户的信息就作为值对象的值嵌入到了保单实体中。
共 12 条评论23 - stg6092019-10-29首先,几乎每个留言都会评论!必须给个大大的赞! 其次,我有3个疑问, 1. 聚合中可能会有实体,那允不允许直接把实体类型作为属性的类型或返回值暴露给外界? 2. 如果允许,那外界就可以直接获取其中的实体,然后调用者可以直接使用实体中的一些相关方法?这样似乎就违反了聚合很的设计? 3. 如果不允许,那外界如果需要得到某个实体的数据,要怎么操作?封装成 DTO 吗? 比如: Aggregate a 中包含一个b属性,b是一个实体。b 中包含操作该实体的方法 M。那外界调用a.b 就会直接获取到 b 这个实体,然后可以直接调用 M 方法。展开
作者回复: 非常感谢! 首先你说的外界,要明确一下,这个外界是微服务内,不同的层之间?还是指微服务之间,也就是微服务外部调用。 如果是在微服务内,不同层之间的话,由于运行在同一个微服务内,实体是可以被其它高层服务获取的,这时实体以DO对象的形式,存在于应用层和领域层,你可以使用DO对象的方法。但是不建议将实体的方法编排放在应用层,而是在领域层封装成领域服务后暴露给应用层。 如果这个外界是微服务之间的话,你需要将实体DO数据转换为DTO对象后,才能被外界使用,这时,外界是不能调用实体的方法的,因此可以隐藏实体核心业务逻辑的实现方式。
21 - 李二木2019-10-23什么是充血模型
作者回复: 说到充血模型,就离不开贫血模型。 先说一下贫血模型吧,贫血模型是指使用的领域对象中只有setter和getter方法,所有的业务逻辑都不包含在领域对象中而是放在业务逻辑层。 而充血模型将大多数业务逻辑放在领域实体中实现,实体本身包含了属性和它的业务行为,它在领域模型中就是一个具有业务行为和逻辑的基本业务单元。
共 5 条评论13 - 美美2019-11-06老师,想请教一下 我们的场景是:商家后台上,商家可以进行门店管理,收银pos机相关的设置,营业相关的设置 背景:商家后台已经存在,且商家基础系统、门店基础信息、pos相关信息,数据模型已经存在,各调用方是直接访问数据库的 目标:将商家、门店的基础信息,经营设置信息,抽象出来形成商家域 统一对外提供能力 想通过DDD的思想来进行建模,感觉无从下手,麻烦老师提供点思路展开
作者回复: 后面会有建模方法的介绍。 你可以试着从用户旅程入手,根据流程看看会发生哪些领域事件,再找找是哪些命令触发这些领域事件的。梳理的差不多的时候,你就找找这些命令都是哪些实体的行为,这样就可以找出好多实体。根据实体可以找出聚合根,再根据聚合根找出关联的实体和值对象。这样聚合就找到了,然后对聚合划分限界上下文。这样就可以设计微服务了。
共 2 条评论12 - 密码1234562019-10-23聚合。用界限上下文把细粒度的实体圈起来当做一个组织,选出组织的董事长。一个组织和另外一个组织交流的时候,只需要通过一个董事长,能够了解该组织的全部非隐私信息
作者回复: 类比的不错。
12 - 张迪2019-10-29跨多个实体的业务逻辑通过领域服务来实现。 其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。 怎么感觉 聚合根做的事情和领域服务一样?
作者回复: 两个出发点不一样,聚合根主要是从实体关联的角度,关注数据一致性。领域服务主要组合的是实体业务行为。
9 - Geek_7c49532020-08-14老师,对于关联两个聚合根的一种关系,应该划分到哪个聚合? 比如: 用户聚合根和课程聚合根。 用户收藏了一门课程,收藏这个操作关联了用户和课程,同时生成了一条收藏记录。 那么收藏记录是值对象还是实体,应该归于哪个聚合根? 如果作为实体,它似乎不需要ID去标识,因为一个用户ID,一个课程ID就可以标识这样一条记录。 如果作为值对象,又感觉不合适,因为我的感觉(仅仅是感觉)多值的值对象不应该能无限增长,但收藏这个操作是不可能限制数量的。展开
作者回复: 我感觉收藏应该也是一个独立聚合,收藏会有聚合根,它会引用用户ID和课程ID这两个值对象。这样可以对用户和课程独立管理和维护,保证用户聚合和课程聚合的领域逻辑稳定。用户登录时,获取用户ID,然后将用户ID值对象作为查询条件从收藏聚合获取收藏记录,再通过收藏记录关联的课程ID从课程聚合获取课程的详细数据。
共 3 条评论6 - 蜗牛慢慢爬2019-10-23听了这么多节课,总结一下还是太抽象了
作者回复: 基础篇主要讲解DDD的基础概念和设计理念,所以相对抽象一些。后面会有中台业务建模和微服务设计案例,比较好理解。敬请期待。
共 2 条评论6 - geek_time2020-06-23欧老师的课程,都是高级工程师和架构师级别的人购买,受众少,讲这么好,销量却不高,可惜了。
作者回复: 感谢哈,帮忙多推荐哦。
5 - 静心2020-08-26关于聚合和聚合根还是没完全理解,请教老师: 既然聚合和聚合根都是实体且具有全局唯一ID,那么聚合和聚合根应该在代码层放置在domain的entity目录下,且应该对应一个class。 按照聚合与聚合根的规则,那么聚合与聚合内实体的关系应该是一种组合关系。 但现实中还会有一种实体间的关系,这种集合与集合中的实体不是组合关系,而是一种列表和列表中元素的关系,如,文章列表与文章的关系,这种关系下,文章列表是没有唯一ID的。那么,想请教老师,对于这种关系,DDD中是如何表述的?文章列表是作为一个实体类存在好呢?还是把列表做成一个文章实体的方法好呢?展开
作者回复: 在DDD的领域模型中聚合是一个逻辑边界概念,聚合本身没有业务逻辑实现相关的代码。聚合的业务逻辑是由聚合内的聚合根、实体、值对象和领域服务等来实现的。聚合没有ID,它不是实体,所以它没有class,就跟企业里面的部门一样,它只是一个组织名称上的概念,在部门内部的人员才是实体,部门内的若干人员一起构成聚合。每个聚合内有一个聚合根,多个实体、值对象和领域服务等领域对象,它们共同完成聚合的业务逻辑。 不清楚您说的文章列表所在的场景是什么样的?是查询后的文章清单结果吗?如果是查询列表结果的话,这个不是领域模型的概念。 在DDD中领域模型可以理解为在某一上下文边界内,由若干业务实体、行为或者由若干业务实体或行为组合而成,完成某个单一职责业务能力的对象组合。我们可以在领域模型内定义对象的依赖和组合关系。
共 2 条评论5 - winzheng2020-03-26老师,投保人和被保人为什么被识别为值对象?他们是客户,具备实体的特征。
作者回复: 它们的新增和修改是在客户的聚合里面。在投保单聚合里面它们就是值对象,它们的数据来源于客户聚合,不允许在投保单聚合单独修改,只可整体替换。
共 5 条评论4 - 祥敏2019-11-01您好,第四讲和第五讲都在讲述微服务内部的拆分与设计,是关系紧密的两讲,反复听了有三四遍略有收获和疑问。 从业务顺延的角度拆分,聚合、聚合根、实体、值对象这四个概念很好的模拟了业务的世界,就像面向对象所讲的一切皆对象,对象与对象之间的关系(聚合)。 疑惑有两点:持久化和实体之间关系的灵活性。 持久化(以关系型数据库为例):聚合内部强一致性,聚合之间最终一致性。聚合是由多个遵循一定规则的实体组成,实体的描述由值对象组成,这样的问题在于聚合、实体、值对象和数据库之间的对应关系不够直接,同时事务管理也比非DDD的方式复杂,如果严格遵循DDD的原则去做,可能会在对象->数据库这个关节会有较多的问题要解决。 实体之间的关系:跨聚合之间实体不能直接发生关系,这个是否会不够灵活,实践中是否会引发一些问题?展开
作者回复: 如果数据一致性没什么问题,那就选择自己最合适的方式去做吧。但是一定要记住,边界要清晰,聚合之间的服务和数据不要耦合,耦合度太高,以后领域模型和微服务演进就很难。
共 3 条评论4 - 马里奥的马里奥2020-11-19文中提到“聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。”,但是在画的图中,投保单聚合根依赖报价单实体,那不就违反了只能通过聚合根访问实体吗?我想知道投保单是通过什么方式来访问报价单实体的。
作者回复: 在投保单聚合内,报价单属于被投保单聚合根引用的实体对象,如果其它聚合需要查看报价单实体的数据,则需要通过投保单ID查询到投保聚合根后,再找到报价单实体,然后访问报价单实体的数据。 而如果将报价设计为独立聚合的话,那么报价单就是报价聚合的聚合根。这样在投保单聚合将报价单设计为值对象可能会更合适一些,投保单聚合中的报价单值对象来源于报价单聚合。
共 2 条评论3 - 勤快的懒洋洋2020-03-07聚合里直接调用仓储吗,还是由应用服务层调用,为什么
作者回复: 仓储接口可以由实体方法调用(一般是聚合根),也可以由领域服务或应用服务调用。仓储一般是用于新增和修改数据,新增和修改数据通常有聚合根来统一管理,可以放在聚合根的方法里面,也可以用聚合内的领域服务来统一管理,通常对于实体较多的复杂的聚合,写入数据库前的DO到PO的转换或者PO到DO的实体数据初始化由工厂服务来统一管理。复杂查询一般不放在聚合内部,可以通过应用服务直接调用仓储查询接口来实现。
共 3 条评论3 - Jie2020-01-172天里一口气把01-05都看了,不仅仅是文字本身,留言回复都大大补充了专利内容,必须给这么认真的老师点个赞
作者回复: 谢谢!
3 - 睁眼看世界2019-10-23老师,就基础篇。我的理解是基于事件风暴(头脑风暴)定义出实体与值对象,并能识别出根对象,进而得到聚合,并清晰领域边界、松耦合。从而达到服务化确定服务的边界!但是这其中事件风暴如何组织进行是否有策略或者说工具技术。如果团队成员就事件风暴无法达成共识,如何进一步推进?
作者回复: 你好,事件风暴会有专门的一节介绍。
共 2 条评论3 - 周大仙2021-12-10老师,我已经将基础篇的5讲都已经看过了,我在第五章总结一下所有问题,一并发问,麻烦老师有空看看。 场景:用户 与 地址 代码:按ddd原则设置 ----==== 华丽的分割线 ====---- public class person { public Interger id; public String name; ... public Address address; // 其它方法 } public class Address { public String province; public String city; ... } ----==== 华丽的分割线 ====---- 问题1:引述“第05篇的聚合节点”,...,值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是 个体化的对象,它们的行为表现出来的是个体的能力。 以上述代码为例,对实体的状态和特征进行描述,结合代码如何理解这句话。 问题2:若完全按照DDD的说话,数据表的设计应设计成单表,且在特殊场景中,如“第4篇”的“案例2”,以序列化对象的形 式存储。若我现在的需求是查出某个省或市等的人员有多少,是否应按范式对数据库进行设计,若是,在程序输出 person实体给其他程序使用时,是否应由代码查询 person 与 address的持续数据,最后组成ddd所说的person对 象并包含address值对象会好点,若是,那我们是否应一开始就应该按照数据库范式设计,以避免将来业务需要变 更,需要改动数据库表的结构和代码才能实现新需要。 问题3:引述“第01篇”的“战略设计”和“战术设计”以及“三步来划定领域模型和微服务的边界法”, 如果我没理解错的话,三步法与“战略设计”、“战术设计”不能对应该上,甚至在实践中可以忽略“战略”和“战术”的 概念。因为三步法中的,第一步是输出领域实体等领域对象,第二步是输出聚合对像,第三步是根据语义将多个聚 合组成领域模型。综合所述,三步法是一种自下而上的实践方法,而“战略”应该是一种更高维度的自上而下的方 法,“第01篇”中未给出自上而下的实践方法。不知道我有没有理解对呢? 问题4:引述“第02篇”桃树分解领域过程,再结合第01篇”的“战略设计”概念,我觉得分解桃树是一个自上而的分解领域过 程,符合“战略设计”的概念,但在实际应用中即难以应用,因为“桃树”是一个成型的产品,几乎不需要维护,所以 能够自上而下去分解领导,但现实中我们的应用是不稳定的经常变动,以上述"persion"和"address"为例,一开始 我们可以单表的形式,去编写实体代码,只需要符合,从用户中查出他们的地址信息。后来需求改变,需要查出某 个省或市的所有用户,这时候我们就得再编写一个Address带唯一id的实体,并且影响到Person,因此我觉得自上 而下地划分领域难以应用在实际开发中,并且在设计时应该要考虑到以后应用的变化,尽量使用范例设计数据表, 用程序处理值对象,不知道我这样的理解对不对? 问题5:假设,以“persion”和“address”为例,在某些特定的场景下,我是否可以把它分成person领域和address领域, person包含address值对象,address包含person值对象,老师请问一下我的理解对吗? 还有就是再极端一点的情况,假设person是在db1,而address在db2,然后person是基于spring cloud开发的, 是一个独立的服务,同样address也是,那这时候老师是建立在各个微服务中采用多数据源的方法查person和 address的持续数据呢?还是通过feign跨服务查询自己实体所需要的值对象呢? 在此,先谢谢老师的解答,学生不胜感激展开共 1 条评论2
- SkyeDance2021-02-23聚合的设计步骤和设计原则特别有指导价值,在设计聚合时,不是将有关系的实体和值对象作简单的组合,最好能够找到实体和值对象运行的业务规则;换种说话,在判断聚合设计是否合理时,看看能否在聚合中找到这样一套业务业务规则。作者在评论中给了一个很简单的例子:「关于订单和商品的不变性,其实里面是有一个重要的规则,也就是订单总金额一定会是所有商品数量乘以价格后的总和,商品是依附于订单存在的。」 在实际操作过程中,有特别多的人关注聚合根中的值对象到底要不要冗余多余的属性信息,比如保单中的投保人和被保人,是只指存储客户编号呢,还是存储一个客户对象,如果是前一种,如何根据投保人名称来查询保单呢?其实这两种方式都可以,解决查询问题的方法也很多,只要你不只是盯着数据库来思考。比如,只存储了客户编号如何解决查询问题?只要在前端用下拉控件选择客户来代替输入客户名称即可;如果保存了客户名称如何在客户信息更新后同步到其他对象中呢?既可以使用定时任务刷新,也可以使用消息广播的方式来同步更新客户信息。展开
作者回复: 是的。 在值对象设计时,可以适当的采用数据冗余的方式。数据冗余后,在当前聚合就可以获取部分值对象的关键数据,这样在需要查询值对象数据时,就不必进行跨微服务调用了,既可以减少微服务之间的调用频率,也可以实现故障隔离。只有当需要获取明细数据时,才通过聚合根ID去其它微服务获取。
2