戏说领域驱动设计(廿二)——聚合

聚合的自白

  大家好,我是聚合,在你们的期盼之下我终于出来了。其实早就想和大家见一面,不过作者每天总想着水流量,到现在才让我出来。他把实体和值对象这两个我家庭内的成员先介绍让我感觉非常的不公平。没有国哪有家?没有家庭,生活也不会温暖。好多的工程师眼里只想着实体他们,让我难受的想要哭泣。明明是由于我的存在才让实体和值对象得到保护,才让家庭内的各个成员不遭受风雨的侵袭,可偏偏总受不到重视。当然,我知道我是一种虚拟的存在,不像实体那样看得见摸得着,这可能也是人们无法快速理解我的主要原因。

  我的家庭不大,我也不喜欢太庞大的家庭,每个人在其中都承担着不同的责任、都在努力的工作。无论外面变化多么快速,在家的保护下每个人都很幸福。我多想永远的将这种平静保持下去,可是树欲静而风不止,总会遇到一些不负责的主人,他们随意的肆虐着我的家庭,不是把我的家人分裂出去就是随意的往家里面添加不属于我们的人员。我们很内向,很害怕与陌生人沟通,也不希望没有关系的人随意的进出我的家门。各位人员都驻扎在这个小小的家中,它让这个家庭的负荷远远超出了我们所能承受的上限,也让家庭成员得不到应有的爱护,家已经不是幸福的代表了。

  我的家庭成员都很羞怯,可是过日子总是需要与外人打交道,所以我选择了一个叫“聚合根”的人作为整个家庭的代表,这是一个体面、漂亮、善于沟通的小伙子,喜欢他的人都叫他“阿根”。他是我们的寄托,也是我们与外界沟通的唯一通道。虽然每个家庭成员仍然需要为工作而努力,虽然阿根时常也会把活推给其它人去干,可是大家都很开心,因为他是公正的,他也在默默的为整个家挺付出,承担了一个家庭代表该干的事情。我生活的这个世界有许多不同的家庭,每个家庭都有自己的习惯、喜好和隐私,所以当你需要与我们的家庭合作的时候,请不要直接插入到我的家中来,也不要试图绕过阿根而联系我的家人。您可以联系阿根同志,电话:010-12345678,他是一个诚实的人,一定会在不违反原则的情况下百分百完成您所嘱托的事情。

  谈到家规,这是我引以为豪壮的,尽管我从来没拥有过七房太太。您知道吗?之所以这个家庭能够工作的很好,一是因为我明确规定了每位成员的责任,令人开心的是每位家人都可以自觉在阿根的指挥下工作;另一方面,我们有一套自己的家庭规则,人人都在遵守。尽管外面形势恶劣,风大雨大,每个家庭成员都不需要担心会受到影响,还是因为阿根,他可以帮我们挡住所有可能破坏家庭正常运转的外部侵扰以及那些影响家庭稳定的人或事件。什么?家庭规则是什么?这个啊,就是一叫法啦,其实人类生活的世界中有许多类似的东西。国家的层面叫宪法,家庭的层叫家法,公司的层面叫制度,软件中叫业务规则。如果把我的家庭放到软件中,那么就是由我来维护业务规则的一致性喽,虽然我不像阿根那么能说会道,我可是规范着整个家庭的运转呢。

  马克思不是说“一切事物都是运动”的嘛?其实我的家庭和家庭成员也一直都在变化着。同人类一样,我也会随着时间而变化,也会生、老、病、死;有需要我们参与的活动时,所有的人都会根据阿根的安排各自调整自己的状态,当然也包括阿根自己。我希望我的家庭是一个整体,我不喜欢个人主义,有了问题大家一起承担,我决不允许某人得利某人却利益受损。我有一个做软件开发的朋友,他说很羡慕我家的一损俱损、一荣俱荣的模式,还说在他们的工作领域有个叫“事务”的东西,和我的家庭是一样的,看来我的家规还是很科学的嘛。

  其实我是一个极端的家庭生活拥护者,我十分不喜欢被随意打扰,也很讨厌被人管制,总会有些好事之人跳出来让我们和其它的家庭合作。增加人际交往其实挺好的,但我顶不喜欢他还让我们和别的家庭成为一种所谓的“利益共同体”。真是搞笑, 我就是一个自私的人。我只关心自己家的利益,别人的生死与我无关,我又不是叶文洁这个圣母婊。再说了,你想要利益共同体也得找个专门的人进行负责协调啊,不是有什么“Saga”吗?那个天天嘴里喊着:“我是大公之人,我要以德服人”;要不你也可以找“领域事件”这个碎催啊,他最喜欢干这种跑腿儿送信儿的事情。其实我挺冤枉的,让我变得自私也是有原因的。几年前我有过一些不堪回首的经历,有人家伙强制的促成了我和另外家庭的合作,期间大家都挺快乐,可一到决策拍板时刻就总会出现变卦,有另外一个家伙暗中使用手段更改了合作伙伴的信息使得他单方面悔约,虽然我家并未受到影响,但每个成员还是很沮丧的。那个做软件的朋友又跳出来说这个叫“并发”,也不知道为什么怎么哪里都有他,反正从那之后我就拒绝再同其它的家庭做什么所谓的“利益共同体”。再有人这样乱指挥, 我就用那个朋友教我的用“最终一致性”回怼,虽然我不知道这是什么意思,但想来朋友应该不会欺骗我吧。

  尽管我不喜欢被打扰,但活在这个世上总会需要和别人打交道,我们其实也不愿意孤独地生活着。幸好,还是小根同志。当我们需要找到对方家庭时,只需要记住那个家庭代表的身份证号就行了;如果对方需要找到我们,也只需要记住阿根的身份证号。放心吧,我们生活的世界虽然乱了一点,但还不会总干一些泄露身份证号卖钱的事情。我喜欢通过只记住对方家庭代表身份证的方式联系彼此,因为我是个单纯的人,也喜欢单纯的事情。我不想进入别人家庭的内部,也不喜欢别人进入我家。有了对方的身份证号,我只需要在黄页中搜索一下就知道对方的信息了,也许不是全部,但谁家没有点秘密呢。

  说了这么多,我还没有介绍自己的家庭成员构成。我的家中只有实体和值对象这些成员,总有些人喜欢往我的家中强行塞入其它的东西比如联系人黄页。我可不喜欢这样干,那个又不是我家的私有财产,是多个家庭共用的,我的自私只是因为我想保护自己的家。很多人没有明白,每一个聚合家庭都是独立自主的,虽然我们也会和外界沟通。再次郑重提醒一下:请不要随意安插外界成员到我的家中,有事儿请电联阿根。对了,我生活在数据的世界中,这个世界有无数和我们类似的家庭。统治者为了避免人口的膨胀,我们没有工作的时候常常会进行休眠,有了任务了我们就会被唤醒,不过请一定要注意我的家庭的完整性。所有的成员都是一体的,不要让一个残缺的我们去面对这个纷扰的世界。

  以上是我——一个数据世界中聚合的自白,希望你喜欢!

IT朋友的自白

  大家好,我是聚合的朋友,无名氏而矣。我这个朋友脑子那里有点问题,啰啰嗦嗦的不知道说什么呢,今天早上还说赵家的狗又多看了他一眼,疯了。咱们这可是技术性文章,哪里管得着个人的荣辱。可作为朋友,也不能对他不尊重,所以还是让我来说一下我的聚合朋友在DDD中的角色和设计原则,我不像他一样脑子有问题,技术型文章自然要用技术语言。

1、隔离

  聚合最为重要的特质是隔离,近一步说是把实体和值对象进行分组。还记得我们前面文章中说过的DDD中的四个隔离级别,聚合隔离是最低层隔离,成本最低但效果也显著。客观来说,聚合其实是一个虚的概念,我们并没有一个叫做聚合或分组的对象把领域模型进行分割。能进入到某一个聚合中的对象都应当在业务上是高内聚的,在汇聚在一起的模型中选择一个代表作为访问聚合的入口,这个代表称之为聚合根,也是我朋友说的“阿根”。虽然在聚合内部可能会包含许多的值对象或实体,但一旦聚合成立,你就需要把这个聚合作为整体来看。任何的改变都不可以违反聚合自身的业务约束也就是不变条件。此外,你还需要保障聚合在持久化时使用事务来管理数据一致性,不过尽量不要使用一个事务同时管理多个聚合,这样会使得事务范围大,加大了并发的概率,性能也不好。这个原则放在过去10年,也许您还有打破的理由。当今的系统动辄数十上百台服务器,也不差再多个几台来安装MQ。返回到隔离这个主题:聚合通过对领域模型分组以实现业务上的隔离;通过使用单一事务原则,能够实现数据处理上的隔离。这两重隔离,可以让领域模型间相互影响变得非常小,实际上做到了最大化的解耦。

2、规模

  由于没有硬性条件来限制聚合的大小,所以在设计过程中一不小心就可能把聚合设计的非常大,如下列代码所示。

public class Account extends EntityModel<Long> {
    private Role role;
    private Department department
}

  客观来讲,这段的代码非常符合面向对象设计精神,但在DDD中这种设计问题却非常大,其中最重要的一点就是聚合的粒度不合理。上面强调过聚合的整体性,如果换成技术的语言就是当你存储或加载一个聚合的时候,必须要全部加载或保存其内包含的各类值对象或实体对象。您也许听说过“懒加载”概念,这并不适用于聚合,因为这个概念是技术层次的,通常会是用在数据实体的操作上。极端情况下一个聚合可能包含了数以万计的值对象,此时您需要进行妥协,比如把这个聚合进行拆分,也不要使用懒加载的方法。

  以上面的代码为例,由于聚合的整体性约束,您在反序列化时除了考虑“Account”所关联的内聚属性,还需要把“Role”和“Department”两个实体一并加载。咱就不说别的,至少查询三个表吧?问题是花了不少力气把数据查询出来了,您可能根本就不去使用,每一次加载的动作大概率有60%的工作是徒劳的。查询还相对简单一些,那如果涉及更新或存储的时候呢?“Role”和“Department”这两个实体需不需要和“Account”一起更新?不更新吧,那你这个还算是一个聚合体吗?更新吧,你凭什么跨BC更新其它的实体?即便是有这个需要,微服务架构下你就得使用分布式事务,系统的性能又降了好多。再退一步,即使这些聚合都在一个BC内,聚合越大事务的范围越大,影响的表越多,怎么看大聚合都是费力不讨好的。

  所以结论出来了,你需要保持对聚合规模的关注,尤其是大聚合的时候需要考虑是否有必要。当然,你也别极端的让每个聚合都只包含一个实体,那样会造成业务内聚性不足。另外需要说明的是聚合中所包含的对象,一般应以值类型为主。由于聚合根是实体,所以很少会出现聚合中再包含另外的实体,在我所做过的项目中的确有遇到过,但总的来讲比例非常少。如果不负责任的去评估,一个聚合中90%都是值对象,这个比例只高不低。

3、聚合根

  聚合中需要指定一个实体作为聚合根来作为整个聚合的对外触点,也就是说外部只能通过聚合根实现对内部对象的访问,这样的限制可以对内部对象实现最大化的保护。此外,聚合根在确保对象不变特性方面起到的作用是巨大的,来自于外部的不合理请求完全可以在聚合根这一层面上进行拦截。不论聚合的规模大与小,都需要选择一个聚合根。假如你用过类似的Axon框架,聚合根都要求从Axon中定义的聚合根抽象类继承。抛开框架,实际上并没有哪个标记限定或标明谁是聚合根,所以一般可以从对象的方法上体现,比如公有方法多比较多;也可以从业务上定义上出来,谁是业务主体谁是聚合根。

  如果一个聚合需要与另外的聚合建立关联关系,只能使用聚合的ID。通过聚合ID方式进行关联有点类似于数据库中外键,其实并不太符合面向对象精神。设计小聚合是在聚合设计过程中要遵循的规范,而通过聚合ID的方式实现聚合间关联可以在代码层次上有效的限制住聚合的大小。此外,由于我们没有持有另外聚合的引用,也自然不会发生同一个事务更新多个聚合的情况,可以避免前面所说的由于大聚合所带来的各种负面问题。

  我在初次使用DDD落地的时候,那个时候其实还没有IDDD这本书,聚合之间的关联使用了引用的方式。当时遇到的一个需求是:雇员包含了角色,角色中包含了部门对象(因为要限制角色的范围),部门对象又包含了雇员对象列表。在测试“查询用户”的场景时系统直接抛出了一个StackOverflowException,虽然找到了问题,也修改成使用ID作为关联。但当时我看着这种设计特别别扭,觉得有反OOD原则,又是初次深度面向对象编程,只想追求纯粹,纠结了好久。直到后来在看到IDDD的时候,才发现当时自己的行为是歪打正着。之所以举这个案例,是想让大家了解理论的重要性。如果你做的东西有了理论基础的支撑,后面就会少走很多的弯路。

  回归正题,我们要求聚合只能通过ID进行关联,但对象导航还是一个刚性的需求。比如账户中关联了角色ID,我们在查询账户权限的时候还是需要通过角色ID把权限实体查询出来。此等场景下,请将查询的操作放到应用服务中,千万别在你的聚合中引入资源仓库对象。

  另外需要再补充说明的是聚合根的识别,这个没有公式。首先,聚合根一定是实体;其次,通常会使用聚合中的业务主体来承担聚合根的角色,而聚合的主体通常也是聚合的名称。比如订单聚合,聚合根是订单实体;账户聚合,账户实体是聚合根。应该没有人会傻到使用实体中的某一个值对象作为聚合根吧 ?

4、事务

  前面我们已经讨论过聚合的事务,但还是有必要再着重解释一下。一个事务中更新多个聚合所引发的负作用我们已经说过,所以不作过多的解释。在单体架构情况下,程序员习惯使用数据库这种刚性事务来保障数据的一致性。到了微服务架构下,这种手段往往不能生效。两个服务很可能使用了不同的数据库,如果都为关系型数据库还能通过使用如2PC、3PC这种基于XA协议的分布式事务,但引入的性能问题还是很大的;如果使用了NoSql,那数据库级别事务也基本不用考虑了。微服务架构的引入,其实也要求我们在考虑问题的时候可以转换一下思路,毕竟事务还是很重要的,因此也就出现了“柔性事务”的概念,也就是我们常说的“最终一致性”。新思想的出现,又引出了所谓的“领域事件”的概念,分布式事务在微服务的架构下也由原来的2PC这种变成了Saga。当然,对于事务其实您有很多种选择,比如TCC,但Saga已经成为了事实上的标准。可是,事情并不如想像中那么美妙,领域事件通常会基于消息队列来实现,在提交本地事务后需要发送一个消息让其它的事务参与者进行自己的事务。那么问题来了?如果本地事务提交成功,但发送消息到队列失败或消息服务器宕机,要如何解决?如果同一个消息被发送了多次要怎么办?这些问题又引入了一系列的解决方案比如:本地消息表、支持事务型消息队列、消息幂处理等。您看吧,虽然只是一个事务问题,确引发了一场不小的技术改革。

  再回到聚合事务的话题中,两个原则需要遵守:1)一个事务只能更新一个聚合;2)使用最终一致性实现多个聚合的事务,即使在一个BC中也尽量不要使用数据库的强一致性事务。

  谢谢各位观众,我的自白完毕。我觉得相对于聚合的自白,我的应该更加通俗易懂,咱好赖也是个老司机了。其实如果再细琢磨琢磨他的话,貌似多少也有一些道理,只可惜他那种表达不够理性,缺乏科技型文档所要求的严谨性。

总结

  本章使用了一种独特的方式书写,纯粹的理论而且几乎没有任何代码。在此不免吐槽几句,许多的程序员太急于求成了,在没有理论的情况就开始落地DDD,难免他们会报怨说DDD不靠谱。主要原因还是缺少理论的支持或理解不够。DDD最为核心的部分是其指导思想,作者常年在OO的圈子混,他说的一些东西我们不了解很正常,即使是从科班出来的工程师最开始学习的也是基于数据库的编程方法,所以就需要对DDD理论进行细致解读才能更有效的在建设系统时使用。比如IDDD(实现领域驱动设计)这本书,里面几乎全是精华,字很小还500多页呢。您可能也看到网上有许多关于DDD的文章,大部分都以理论为主,也不是说作者写的不好,而是DDD本身就是理论的集合,你很难在不积累理论的情况下来有效的实施DDD,仅仅在看一些代码案例后就开搞,最终出来的东西也是东施效颦,不会在DDD中获益。另外,您也可能在微信或QQ群中见到一些大牛,不要只羡慕他们的能力,人家疯狂学习的时候您都没看到。最后劝您一句:把浮躁的心收一收,踏下心来学习一下理论,不仅DDD如此,任何一门学问也都是一样的。

 

 2 total views,  1 views today

页面下部广告