从本节开始,我们将讨论HealthMonitor案例系统的战术设计,首先分析核心的领域模型对象,包括聚合、实体和值对象。
设计领域模型最基本和最重要的工作是在限界上下文中识别聚合。聚合定义了限界上下文中的一致性范围,即聚合由一个根实体和一组实体/值对象组成。从架构模式上看,我们可以将聚合视为工作单元(Unit of Work,UoW)架构模式的一种应用。
1.识别聚合
为了识别限界上下文中的聚合,我们需要对业务场景和需求做进一步的深入分析,从而形成粒度更细的通用语言。我们在1.2节中以Monitor限界上下文为例,用通用语言描述了一系列业务需求。
基于这些业务需求描述,我们获取了大量有用的信息。从描述中可知健康检测单(HealthTestOrder)是一个潜在的聚合,因为它是用户进入Monitor限界上下文的入口,同时包含着健康计划。但是,健康检测单本质上只是代表着用户申请健康检测的过程以及对应的状态。如果你注意到业务需求描述中的最后一点,你会发现健康检测单并不适合持有代表用户健康检测结果的健康积分(HealthScore),因为健康积分是一个随着健康任务的执行而不断变化的对象。
那么,是不是把HealthTestOrder和HealthScore都设计成聚合呢?显然也不合适,因为根据上一章中关于聚合设计思想的讨论,我们需要确保聚合内部对象的状态一致性。显然,HealthTestOrder和HealthScore之间的状态应该是一致的,在HealthTestOrder状态变更的同时,HealthScore也需要符合一定的业务规则,不能出现HealthTestOrder已经关闭,而HealthScore还在不断增加的情况。
接下来,我们把健康检测单和健康积分这两个概念融合在一起,创建健康监控(HealthMonitor)。HealthMonitor就是Monitor限界上下文的聚合,也是整个案例系统名称的由来。这里不直接使用Monitor这个名字作为聚合,原因在于Monitor的概念过于广泛,很难准确突出当前业务场景下的核心概念。
与之类似,我们也可以明确案例系统中其他各个限界上下文中的聚合,如图3-8所示。
图3-8 HealthMonitor案例系统中限界上下文的聚合
2.确定聚合标识符
我们知道任何一个聚合都需要有一个全局唯一的标识符。针对HealthMonitor案例系统,我们可以明确如图3-9所示的聚合标识符。
图3-9 HealthMonitor案例系统中的聚合和聚合标识符
在面向领域设计中,每个限界上下文通过一组包含实体和值对象来表达领域逻辑。接下来,让我们基于HealthMonitor案例系统来实现这些领域模型对象。
实体对象可以分为两种:一种是只出现在某一个限界上下文中的实体,我们称之为专享实体;另一种则可以供多个限界上下文一起使用,我们称之为共享实体。
在本节中,我们也基于Monitor限界上下文讨论实体对象。在Monitor限界上下文中,HealthTestOrder是一个比较容易识别的实体,因为每个HealthTestOrder势必会具备一个唯一标识,我们也可以变更它的状态。同时,健康计划也应该是一个实体对象,但在Monitor限界上下文中的健康计划显然和Plan限界上下文中的聚合对象HealthPlan不是一个概念。在Monitor限界上下文中,健康计划只需要包括制定医生、计划描述、执行周期等信息,其内部包含的健康任务信息并不需要体现在这个限界上下文中。因此,我们使用HealthPlanProfile来表达这层关系。当然,HealthPlanProfile应该持有一个唯一标识符,所以它也是一个实体对象。
现在,在Monitor限界上下文,让我们在聚合的基础上添加实体对象,如图3-10所示。
图3-10 Monitor限界上下文中的聚合和实体对象
在涉及多个限界上下文交互的场景中,有时候会出现共享实体概念的情况。前面介绍的HealthPlanProfile就是很典型的一个例子。在Monitor限界上下文中,我们通过HealthPlanProfile为聚合HealthMonitor提供健康计划相关的数据。而HealthPlanProfile中的数据显然应该来自Plan限界上下文,所以在Plan限界上下文中也应该存在这个代表健康计划的实体。但是,从系统解耦角度讲,我们认为实体对象是不能在两个上下文之间直接共享的,需要引入专门的转换器对实体对象进行转换。
在Plan限界上下文中,我们使用Health-Plan这个名称来代表健康计划。而在Monitor限界上下文中把代表这一概念的实体命名为HealthPlanProfile,其目的就是解耦。图3-11展示了HealthPlanProfile实体的这种定位。
图3-11 共享实体概念与转换示意图
在Plan和Task这两个限界上下文中的健康任务对象HealthTask也是类似的情况。
与实体对象一样,值对象也可以分为专享值对象和共享值对象这两种类型。值对象的识别通常并不困难。在Monitor限界上下文中,我们通过分析业务需求描述可以快速提炼出几个值对象。例如,既往病史(Anamnesis)和症状(Symptom)就是很典型的值对象,这些对象不需要确定唯一的标识符,也不存在任何状态变化的可能性。同时,代表检测结果的HealthScore也是一个值对象。
现在,在Monitor限界上下文中,让我们继续在聚合和实体对象的基础上添加值对象,如图3-12所示。
图3-12 Monitor限界上下文中的聚合、实体和值对象
图3-12中代表既往病史的Anamnesis对象以及代表症状的Symptom对象都是共享值对象的典型例子。在Monitor限界上下文中,我们通过Anamnesis和Symptom来申请健康监控。而这两部分数据也会用来确定用户的最优健康计划,所以在Plan限界上下文中也需要使用这两个对象。图3-13展示了Anamnesis和Symptom值对象的定位以及在不同限界上下文之间的传递过程。
图3-13 共享值对象的示例
请注意,与实体对象不同,我们可以在各个限界上下文中直接共享和传递值对象,而不需要引入专门的转换器。这是因为值对象是没有唯一标识和状态的,值对象的共享不会使系统的运行状态产生任何的变化。