前言

哈喽大家周二好,上次咱们说到了实体与值对象的简单知识,相信大家也是稍微有些了解,其实实体咱们平时用的很多了,基本可以和数据库表进行联系,只不过值对象可能不是很熟悉,值对象简单来说就是在DDD领域驱动设计中,为了更好的展示领域模型之间的关系,制定的一个对象,它没有状态和标识,目的就是为了表示一个值。今天呢本来不想说聚合了,因为网上的资料已经铺天盖地,想着开始说领域服务和领域事件了,但是为了本系列的完整性,今天就简单的说一下聚合和聚合根的理解,,如果你已经很明白了,请指出我说的不足之处,以便可以让大家知道,如果你还不是很明白,请看过后思考以下几个问题,领域事件下次再说吧,这样也就完成了今天的头脑风暴:

1、什么是聚合?

2、聚合的作用是什么?

3、我们平时接触到聚合了么?

这里有一个小Code,大家先看看这三者都是属于什么(实体,值对象,聚合/聚合根):

  public class Order
{
public Guid Id;
public string OrderNo;
public Address Address;
public List<OrderItem> Items;
//...
} public class OrderItem
{
public string Id;
public float Price;
public Goods Goods;
public int Count;
//...
} public class Goods
{
public string Id;
public string Name;
//...
} public class Address
{
public string Id;
public string Country;
public string Province;
//...
}

零、今天完成蓝色区块部分

一、聚合的概念 —— 领域的核心

1、聚合的概念

在DDD领域驱动设计第一次被提出的时候,聚合的概念就随之而来了,在之前的文章中,我们说到了领域和子领域的划分,也说了限界上下文的定义,这些都是和我们平时以数据模型为中心所不同的概念,可能理解起来不是很容易,但是至少我们有了这个影子,想象着一个大的领域项目,根据业务来拆分成了多个子领域与上下文,可能不同的上下文中甚至有相似的概念,举个栗子就是,订单上下文有商品,物流上下文有货物,库存上下文有存货等等等等,这时候你会发现,其实他们都是指的同一个东西,只不过在不同的上下文中被人为的赋予了不同的概念,有的是实体(库存),有的是值对象(订单),但是它们又不是一个概念,因为他们属于不同的子领域。

这个时候,既然我们从大的方面已经对限界上下文进行分离整合,与之而来的肯定是领域模型的分离(我们肯定不能把每一个表放在一起,也不会把他们都一个个并列排开),那既然有分离肯定就是有聚合,这个时候,聚合就出来了,其实DDD提出聚合的概念是为了保证领域内对象之间的一致性问题,因为我们从上边也看到了,在不同的地方会存在调用关系,当然主要还是子领域内部相互调用,

比如创建一个订单,必然会生成订单详情,订单详情肯定会有商品信息,我们在修改商品信息的时候,肯定就不能影响到这个订单详情中的商品信息。再比如:用户在下单的时候,会选择一个地址作为邮寄地址,如果该用户立刻下另一个订单,并对自己个人中心的地址进行修改,肯定就不能影响刚刚下单的邮寄地址信息。

这个时候,聚合就有很强的作用,通过值对象保证了对象之间的一致性。我们平时在开发的时候,虽然没有用到DDD,肯定也是经常用到聚合,就比如上边的问题,撇开DDD不谈,就平时来说,你肯定不会把商品 id 直接绑定到订单详情表中,为外键的,不然会死得很惨。这个时候其实我们就有一些聚合的概念了,因为什么呢,下单的时候,我们关注订单领域模型,修改商品的时候,我们关注商品领域模型,这些就是我们说到的聚合,当然一个上下文会有很多聚合,而且聚合要尽可能的细分,那如何正确的区分聚合,以及以什么为基准,请往下看。

2、我们如何对聚合进行划分

1、哪些实体或值对象在一起才能够有效的表达一个领域概念。

比如:订单模型中,必须有订单详情,物流信息等实体或者值对象,这样才能完整的表达一个订单的领域概念,就比如文章开头中提到的那个Code栗子中,OrderItem、Goods、Address等

2、确定好聚合以后,要确定聚合根

  比如:订单模型中,订单表就是整个聚合的聚合根。

 /// <summary>
/// 聚合根 Order
/// </summary>
public class Order : AggregateRoot
{
public Guid Id;
public string OrderNo;
public Address Address;//值对象
public List<OrderItem> Items;//实体集合
//...
}

3、对象之间是否必须保持一些固定的规则。

比如:Order(一 个订单)必须有对应的客户邮寄信息,否则就不能称为一个有效的Order;同理,Order对OrderLineItem有不变性约束,Order也必须至少有一个OrderLineItem(一条订单明细),否则就不能称为一个有效的Order;

另外,Order中的任何OrderLineItem的数量都不能为0,否则认为该OrderLineItem是无效 的,同时可以推理出Order也可能是无效的。因为如果允许一个OrderLineItem的数量为0的话,就意味着可能会出现所有 OrderLineItem的数量都为0,这就导致整个Order的总价为0,这是没有任何意义的,是不允许的,从而导致Order无效;所以,必须要求 Order中所有的OrderLineItem的数量都不能为0;那么现在可以确定的是Order必须包含一些OrderLineItem,那么应该是通 过引用的方式还是ID关联的方式来表达这种包含关系呢?这就需要引出另外一个问题,那就是先要分析出是OrderLineItem是否是一个独立的聚合 根。回答了这个问题,那么根据上面的规则就知道应该用对象引用还是用ID关联了。

那么OrderLineItem是否是一个独立的聚合根呢?因为聚合根意 味着是某个聚合的根,而聚合有代表着某个上下文边界,而一个上下文边界又代表着某个独立的业务场景,这个业务场景操作的唯一对象总是该上下文边界内的聚合 根。想到这里,我们就可以想想,有没有什么场景是会绕开订单直接对某个订单明细进行操作的。也就是在这种情况下,我们 是以OrderLineItem为主体,完全是在面向OrderLineItem在做业务操作。有这种业务场景吗?没有,我们对 OrderLineItem的所有的操作都是以Order为出发点,我们总是会面向整个Order在做业务操作,比如向Order中增加明细,修改 Order的某个明细对应的商品的购买数量,从Order中移除某个明细,等等类似操作,我们从来不会从OrderlineItem为出发点去执行一些业 务操作;另外,从生命周期的角度去理解,那么OrderLineItem离开Order没有任何存在的意义,也就是说OrderLineItem的生命周 期是从属于Order的。所以,我们可以很确信的回答,OrderLineItem是一个实体。

4、聚合不要设计太大,否则会有性能问题以及业务规则一致性的问题。

对于大聚合,即便可以成功地保持事务一致性,但它可能限制了系统性能和可伸缩性。 系统可能随著时间可能会有越来越多的需求与用户,开发与维护的成本我们不应该忽视。

怎样的聚合才算是"小"聚合呢??

好的做法是使用根实体(Root Entity)来表示聚合,其中只包含最小数量的属性或值类型属性。哪些属性是所需的呢??简单的答案是:那些必须与其他属性保持一致的属性。

比如,Product聚合内的name与description属性,是需要保持一致的,把它们放在两个不同的聚合显然是不恰当的。

5、聚合中的实体和值对象应该具有相同的生命周期,并应该属于一个业务场景。

比如一个最常见的问题:论坛发帖和回复如何将里聚合模型,大家想到这里,联想到上边的订单和订单详情,肯定会peng peng的这样定义;

    /// <summary>
/// 聚合根 发帖
/// </summary>
public class Post : AggregateRoot
{
public string PostTitle;
public List<Reply> Reply;//回复
//...
}
/// <summary>
/// 实体 回复
/// </summary>
public class Reply : Entity
{
public string Content;
//...
}

这样初看是没有什么问题,很正常呀,发帖子是发回复的聚合根,回复必须有一个帖子,不然无效,看似合理的地方却有不合理。

比如,当我要对一个帖子发表回复时,我取出当前帖子信息,嗯,这个很对,但是,如果我对回复进行回复的时候,那就不好了,我每次还是都要取出整个带有很多回复的帖子,然后往里面增加回复,然后保存整个帖子,因为聚合的一致性要求我们必须这么做。无论是在场景还是在并发的情况下这是不行的。

如果帖子和回复在一个聚合内,聚合意味着“修改数据的一个最小单元”,聚合内的所有对象要看成是一个整体最小单元进行保存。这么要求是因为聚合的意义是维护聚合内的不变性,数据一致性;
仔细分析我们会发现帖子和回复之间没有数据一致性要求。所以不需要设计在同一个聚合内。

从场景的角度,我们有发表帖子,发表回复,这两个不同的场景,发表帖子创建的是帖子,而发表回复创建的是回复。但是订单就不一样,我们有创建订单,修改订单这两个场景。这两个场景都是围绕这订单这个聚合展开的。

所以我们应该把回复实体也单独作为一个聚合根来处理:

    /// <summary>
/// 内容
/// </summary>
public class Content
{
public string Id;
public DateTime DatePost;
public string Contents;
public string Title;
//...
}
/// <summary>
/// 聚合根 发帖
/// </summary>
public class Post : AggregateRoot,ContentBase
{
public string Title;
//...
}
/// <summary>
/// 聚合根 回复
/// </summary>
public class Reply : AggregateRoot,ContentBase
{
public Content Content;
public Post Post;//帖子实体聚合根
//...
}

当然这样的话,我们就不能通过帖子一次性全部把回复拿出来,我们就只能单写逻辑了,比如在应用层,但是这样不会对领域层造成失血,因为本来就不是领域的问题。

二、聚合是如何联系的

如何联系,在上文的代码中以及由体现了,这里用文字来说明下,具体的可以参考文中的代码

1、聚合根、实体、值对象的区别?

从标识的角度:

聚合根具有全局的唯一标识,而实体只有在聚合内部有唯一的本地标识,值对象没有唯一标识,不存在这个值对象或那个值对象的说法;

从是否只读的角度:

聚合根除了唯一标识外,其他所有状态信息都理论上可变;实体是可变的;值对象是只读的;

从生命周期的角度:

聚合根有独立的生命周期,实体的生命周期从属于其所属的聚合,实体完全由其所属的聚合根负责管理维护;值对象无生命周期可言,因为只是一个值;

2、聚合根、实体、值对象对象之间如何建立关联?

聚合根到聚合根:通过ID关联;

聚合根到其内部的实体,直接对象引用;

聚合根到值对象,直接对象引用;

实体对其他对象的引用规则:1)能引用其所属聚合内的聚合根、实体、值对象;2)能引用外部聚合根,但推荐以ID的方式关联,另外也可以关联某个外部聚合内的实体,但必须是ID关联,否则就出现同一个实体的引用被两个聚合根持有,这是不允许的,一个实体的引用只能被其所属的聚合根持有;

值对象对其他对象的引用规则:只需确保值对象是只读的即可,推荐值对象的所有属性都尽量是值对象;

3、如何识别聚合与聚合根?

明确含义:一个Bounded Context(界定的上下文)可能包含多个聚合,每个聚合都有一个根实体,叫做聚合根;

识别顺序:先找出哪些实体可能是聚合根,再逐个分析每个聚合根的边界,即该聚合根应该聚合哪些实体或值对象;最后再划分Bounded Context;

聚合边界确定法则:根据不变性约束规则(Invariant)。不变性规则有两类:1)聚合边界内必须具有哪些信息,如果没有这些信息就不能称为一个有效的聚合;2)聚合内的某些对象的状态必须满足某个业务规则;

1.一个聚合只有一个聚合根,聚合根是可以独立存在的,聚合中其他实体或值对象依赖与聚合根。

2.只有聚合根才能被外部访问到,聚合根维护聚合的内部一致性。

三、聚合的一些思考

1、优点

其实整篇文章都是在说的聚合的优点,这里简单再概况下:

聚合的出现,很大程度上,帮助了DDD领域驱动设计的全部普及,试想一下,如果没有聚合和聚合根的思维,单单来说DDD,总感觉不是很舒服,而且领域驱动设计所分的子领域和限界上下文都是从更高的一个层面上来区分的,有的项目甚至只有一个限界上下文,那么,聚合的思考和使用,就特别的高效,且有必要。

聚合设计的原则应该是聚合内各个有相互关联的对象之间要保持 不变性!我们平时设计聚合时,一般只考虑到了对象之间的关系,比如看其是否能独立存在,是否必须依赖与某个其他对象而存在。

2、担忧

我接触的DDD中的聚合根的分析设计思路大致是这样:1、业务本质逻辑分析;2、确认聚合对象间的组成关系;3、所有的读写必须沿着这些固有的路径进行。
这是一种静态聚合的设计思路。理论上讲,似乎没有什么问题。但实际上,因为每一个人的思路以及学习能力,甚至是专业领域知识的不同,会导致设计的不合理,特别是按照这个正确的路线设计,如果有偏差,就会达到不同的效果,有时候会事倍功半,反而把罪过强加到DDD领域驱动上,或者增加到聚合上,这也就是大家一直不想去更深层去研究实践这种思想的原因。

DDD本来就是处理复杂业务逻辑设计问题。我看到大家用DDD去分析一些小项目的时候,往往为谁是聚合根而无法达成共识这说明每个人对业务认识的角度、深度和广度都不同,自然得出的聚合根也不同。试想,这样的情况下,领域模型怎么保持稳定。

不过这也许不是一个大问题,只要我们用心去经营,去学习,去沟通,一切都不是问题!

四、结语

今天简单的说了下聚合,明天就正式开始项目开发,到领域服务和领域事件了,不知道你能否回答文章开头的问题了呢?

 /// <summary>
/// 聚合根 Order
/// 实体有标识ID,有生命周期和状态,通过ID进行区分
/// 聚合根是一个实体,聚合根的标识ID全局唯一,聚合根中的实体ID在聚合根内部唯一就行
/// 值对象主要就是值,与状态,标识无关,没有生命周期,用来描述实体状态。
/// </summary>
/// 属性都是值对象
public class Order : AggregateRoot
{
public Guid Id;
public string OrderNo;//值对象
public Address Address;//值对象
public List<OrderItem> Items;//实体集合
//...
}
/// <summary>
/// 实体 OrderItem
/// 属性都是值对象
/// </summary>
public class OrderItem : Entity
{
public float Price;
public Goods Goods;
public int Count;
//...
} /// <summary>
/// 值对象 Goods
/// 属性都是值对象
/// </summary>
public class Goods : ValueObject
{
public string Name;
//...
} /// <summary>
/// 值对象 Address
/// </summary>
public class Address : ValueObject
{
public string Country;
public string Province;
//...
}
/// <summary>
/// 值对象
/// </summary>
public class ValueObject
{ }
/// <summary>
/// 领域实体
/// </summary>
public class Entity
{
public string Id; }
/// <summary>
/// 聚合根的抽象实现类,定义聚合根的公共属性和行为
/// </summary>
public abstract class AggregateRoot : Entity
{ }

从壹开始微服务 [ DDD ] 之六 ║聚合 与 聚合根 (下)的更多相关文章

  1. 从壹开始微服务 [ DDD ] 之八 ║剪不断理还乱的 值对象和Dto

    缘起 哈喽大家周四好,时间是过的真快,这几天一直忙着在公司的项目,然后带带新人,眼看这周要过去了,还是要抽出时间学习学习,这些天看到群里的小伙伴也都在忙着新学习,还是很开心的,至少当时的初衷已经达到了 ...

  2. 从壹开始微服务 [ DDD ] 之终篇 ║当事件溯源 遇上 粉丝活动

    回首 哈喽~大家好,时间过的真快,关于DDD领域驱动设计的讲解基本就差不多了,本来想着周四再开一篇,感觉没有太多的内容了,剩下的一个就是验证的问题,就和之前的JWT很类似,就不打开一个章节了,而且这个 ...

  3. 从壹开始微服务 [ DDD ] 之十二 ║ 核心篇【下】:事件驱动EDA 详解

    缘起 哈喽大家好,又是周二了,时间很快,我的第二个系列DDD领域驱动设计讲解已经接近尾声了,除了今天的时间驱动EDA(也有可能是两篇),然后就是下一篇的事件回溯,就剩下最后的权限验证了,然后就完结了, ...

  4. 从壹开始微服务 [ DDD ] 之七 ║项目第一次实现 & CQRS初探

    前言 哈喽大家周五好,我们又见面了,感谢大家在这个周五读我的文章,经过了三周的时间,当然每周两篇的速度的情况下,咱们简单说了下DDD领域驱动设计的第一部分,主要包括了,<项目入门DDD架构浅析& ...

  5. 从壹开始微服务 [ DDD ] 之五 ║聚合:实体与值对象 (上)

    前言 哈喽,老张是周四放松又开始了,这些天的工作真的是繁重,三个项目同时启动,没办法,只能在深夜写文章了,现在时间的周四凌晨,白天上班已经没有时间开始写文章了,希望看到文章的小伙伴,能给个辛苦赞

  6. 从壹开始微服务 [ DDD ] 之十一 ║ 基于源码分析,命令分发的过程(二)

    缘起 哈喽小伙伴周三好,老张又来啦,DDD领域驱动设计的第二个D也快说完了,下一个系列我也在考虑之中,是 Id4 还是 Dockers 还没有想好,甚至昨天我还想,下一步是不是可以写一个简单的Angu ...

  7. 从壹开始微服务 [ DDD ] 之三 ║ 简单说说:领域、子域、限界上下文

    前言 哈喽大家好,DDD领域驱动设计系列又开始了,前天周二的那篇入门文章中,也收到了一定的效果(写小说的除外),同时我也是倍感鸭梨,怎么说呢,DDD领域驱动设计已经有十年历史了,甚至更久,但是包括我在 ...

  8. 从壹开始微服务 [ DDD ] 之一 ║ D3模式设计初探 与 我的计划书

    缘起 哈喽大家周四好!又是开心的一天,时间过的真快,我们的 <从壹开始 .net core 2.1 + vue 2.5>前后端分离系列共 34 篇已经完结了,当然以后肯定还会有更新和修改, ...

  9. 从壹开始微服务 [ DDD ] 之九 ║从军事故事中,明白领域命令验证(上)

    烽烟 哈喽大家周二好呀,咱们又见面了,上周末掐指一算,距离 圣诞节 只有 5 周的时间了(如果你还不知道为啥我要提圣诞节这个时间点,可以看看我的第二系列开篇<之一 ║ D3模式设计初探 与 我的 ...

随机推荐

  1. vue学习之响应式原理的demo实现

    Vue.js 核心: 1.响应式的数据绑定系统 2.组件系统. 访问器属性 访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过 defineProperty() 方法单独定义. va ...

  2. Python爬虫-pyspider框架的使用

      pyspider 是一个用python实现的功能强大的网络爬虫系统,能在浏览器界面上进行脚本的编写,功能的调度和爬取结果的实时查看,后端使用常用的数据库进行爬取结果的存储,还能定时设置任务与任务优 ...

  3. fasthttp 的 goroutine pool 实现探究

    引言 fasthttp是一个非常优秀的web server框架,号称比官方的net/http快10倍以上.fasthttp用了很多黑魔法.俗话说,源码面前,了无秘密,我们今天通过源码来看一看她的gor ...

  4. Hadoop问题:DataNode进程不见了

      DataNode进程不见了 问题描述 最近配置Hadoop的时候出现了这么一个现象,启动之后,使用jps命令之后是这样的: 看不到DataNode进程,但是能够正常的工作,是不是很神奇啊? 在一番 ...

  5. BZOJ_4804_欧拉心算_欧拉函数

    BZOJ_4804_欧拉心算_欧拉函数 Description 给出一个数字N Input 第一行为一个正整数T,表示数据组数. 接下来T行为询问,每行包含一个正整数N. T<=5000,N&l ...

  6. BZOJ_3524_[Poi2014]Couriers_主席树

    BZOJ_3524_[Poi2014]Couriers_主席树 题意:给一个长度为n的序列a.1≤a[i]≤n. m组询问,每次询问一个区间[l,r],是否存在一个数在[l,r]中出现的次数大于(r- ...

  7. WeTest----如何使用WeTest进行App性能测试?

    使用Wetest可以测试手机app的性能,wetest主打游戏app测试,但是对于其余的app仍然适用,手机可以root,也可在非root的情况下进行测试, 此时可以获取的性能数据包括:FPS.整机C ...

  8. linux 挂载共享文件夹

    1.背景 通常会有这样的场景,开发人员在Windows编写代码,然后放在linux环境编译,我们通过mount命令就可以实现将代码直接挂到linux环境上去,使Windows上的共享文件夹就像linu ...

  9. Mysql存储过程 —— SEQUENCE的实现

    http://blog.csdn.net/crazylaa/article/details/5368447 创建sql语句: DROP TABLE IF EXISTS sequence; -- 建se ...

  10. 轻量化卷积神经网络MobileNet论文详解(V1&V2)

    本文是 Google 团队在 MobileNet 基础上提出的 MobileNetV2,其同样是一个轻量化卷积神经网络.目标主要是在提升现有算法的精度的同时也提升速度,以便加速深度网络在移动端的应用.