如何运用领域驱动设计 - 聚合
概述
在前几篇的博文中,我们已经学习到了如何运用实体和值对象。随着我们所在领域的不断深入,领域模型变得逐渐清晰,我们已经建立了足够丰富的实体和值对象。但随着实体和值对象的数量逐渐增多,它们之间的关系也显得越来越复杂:实体A与实体B存在一对一的关系,实体B又与实体C存在一对多的关系。就这样一层套一层,本来约束已经足够好的领域对象们彷佛已经开始对我们不太友好。为了处理这一系列的问题,我们需要将一些实体和值对象划分在一个统一的边界内,原来存在多重关联关系的大模型被分解为较小的领域对象群。
而这种强有力的划分手法就是领域驱动设计战术模式中的“聚合”。可能大家已经听过它的一个重要部分“聚合根”,那么我们什么情况下考虑使用聚合根呢?聚合根又是从什么地方来?聚合与实体之间又有什么关系?如何确定和划分一个合理的聚合?本文将从不同的角度来带大家重新认识一下“聚合”这个概念,并且给出相应的代码片段(本教程的代码片段都使用的是C#,后期的实战项目也是基于 DotNet Core平台)。
何为聚合
还是先来看看原著
发现实体关系
根据需求描述,再结合我们已有的领域设计知识,我们马上就能找出另外一个重要的实体对象出来。没错,那就是账单。在这个案例中,我们暂定将账单命名为记账薄(Account book)。在第二个原型图中,我们大致能够理解记账薄是一个什么东西,它记录了行程中所有的开销内容和开销金额。这一行一行的开销信息,我们将它命名为开销项(Overhead item)。这里为了简化起见,我们忽略了每条开销项中的其它信息,例如参与人员,参与地点等等。
接下来,我们来分析已经发现的两个事物:记账薄与 开销项。先来说开销项吧,它是属于实体还是值对象呢?结合前两篇博文中我们说学到的内容,它需要一个ID来辨识它吗? 也许还是有些困惑,因为好像它不像性别、姓名这一类东西具有很明显的无ID特征。所以我们需要来识别该对象拥有的属性:开销内容、开销金额、开销时间。“在2019年10月12日,买了一个冰糕花费了3元人民币”,在我们当前的领域,我们需要使用一个ID来区分它吗?很显然我们是需要的,我们不能说只要在同一时间花了同样的钱买了同样的东西就是一样的东西了,比如用户A在行程A中和用户B在行程B中同时间同样的钱买了同样的东西,我们会认为是一样的吗?很显然,不能。所以开销项是一个实体。那么记账薄呢?很显然,它也是一个实体,我们需要通过ID来识别到底是哪个记账薄。
此时我们已经捕获出了两个实体对象:记账薄 与 开销项。而且可以清楚的看到,它们之间是一个一对多的关系。然后来尝试将它们转换为我们熟悉的C#代码吧:
public class AccountBook { public Guid ID { get; private set; } public List<OverheadItem> OverheadItems {private get;private set; } //ctor // 记账薄的行为 // .... } public class OverheadItem { public Guid ID { get; private set; } //开销内容 public OverheadContent Content { get;private set; } //开销金额 public OverheadMoney Money { get;private set; } // ctor // 开销项目的行为 // .... } public class Itinerary { public int ID { get; set; } public List<Person> Participants { get;private set; } public List<Address> Places { get;private set; } public ItineraryNote Note { get;private set; } public ItineraryTime TripTime { get;private set; } public ItineraryStatus Status { get;private set; } //ctor // 行程的行为 // .... }
OK,此时我们已经完成了记账有关的模型。再来回顾我们之前的行程实体模型:“当旅程建立的时候,则证明该旅程的账单已经被开启了”,因此我们可以看出,旅程和账薄是连接在一起的,一个旅程就对应着其拥有的对应账薄,所以它们是一个一对一的关系。
到目前为止,我们拥有了三个比较明显的实体:旅程、记账薄、开销项目,还有该领域中很多大大小小的值对象。旅程和记账薄是一对一的关系,记账薄和开销项目是一对多的关系。多读一下它们之间的关联关系,唉!!!好累,那是不是再引入一个领域对象进来,就会让它们之间的关系更复杂呢?这样一层绕一层,就仿佛滚毛线球一样,越理越乱了。
开始划分边界吧
我根据目前所涉及的领域对象,大致绘了一个领域之间的图,当然这个图并不是规范的,里面缺少了很多我们已经捕获出来的值对象等等,它只是为了帮助你大致回顾一下我们目前所Get到的领域模型结果:
图中将“旅行记账”的部分于“推荐”的部分用了方块给隔离开来,这个结果我想大家也很容易理解,因为有关推荐的这些东西,比如推荐餐馆呀,推荐花店呀对我们的旅行记账来说并没有太大的关系。关系域于关系域中,我们通过划分了一个合理的边界来隔离它们,那么反过来思考,一个域中的各个领域对象,我们能不能通过一个什么手段来划分它们呢?将它们通过边界的隔离,实现区域内的自治,这样更方便我们来处理它们之间的逻辑关系。
假如用户想查看当前行程的记账薄,按照常规处理我们会怎么办呢? 用户会访问有关记账薄的仓储(仓储的有关概念将在下一篇文章讲解),获取到当前记账薄。此时,用户获取到了账薄的有关信息,比如开销项啊,总开销金额啊等等,但是对用户来说,它是很迷茫的,因为它仅仅获取到了账薄的信息,它不知道这个账薄属于哪次行程,所以它必须又得去获取一下行程的信息。而这种场景往往都是一起出现的,你只要获取账薄你就必须要获取行程。
可能你已经发现了,它们其实可以是一体的。就像开销项和记账薄是一体的一样,行程和记账薄这两个大实体居然也是可以是一体的。而这种关系,就是我们今天的主题——“聚合”。
我们可以将旅行行程、记账薄、行程人员、开销项、行程时间等一系列有关的对象都划分在行程的边界内,因为确确实实它们是属于行程的,一旦脱离了行程它们好像都没有任何意义。
选取一个聚合根
行程和记账薄是一体的,且它们是一对一的关系。如果将这个关系转换为我们熟悉的代码,我们需要将一个类作为另一个类的属性,那么在这个案例中,我们是用行程包含记账薄,将记账薄作为属性呢?还是记账薄包含行程呢? 你也许会说,它们可以相互包含。确实,现在的ORM框架可以运行你将两者互相包含并映射到数据库,但是在这里我们没有必要这么做,因为我们已经知道,