有些时候,约束条件无法用单独一个方法来轻松表达,抑或约束条件中会使用到与对象职责无关的信息,那么我们就可以将其提取到一个显式的对象中。
规格(SPECIFICATION)
很多时候业务规则并不适合作为实体或值对象的职责,而且规则的变化和组合也会掩盖领域对象的含义。但是,将规则移出领域层则导致领域代码无法表达模型。此时,我们可以定义规格(谓词形式的显式值对象),它用于确定对象是否满足指定的标准。规格将规则保留在领域层,由于规格是一个完备的对象,所以这种设计也能更加清晰地反映模型。
规格一般有如下三种用法:
- (验证)验证对象,检查它是否能满足某些标准,比如示例-SMS中成绩实体在修改分数时就需要通过规约判断当前是否满足修改的标准;
- (选择)从集合中选择一个符合要求的对象,可以搭配资源库使用;
- (根据要求来创建)指定在创建新对象时必须满足某种要求。
规格由“谓词”概念演变而来,因此我们可以使用“AND”,“OR”和“NOT”等运算对规格进行组合和修改。比如在SMS中,教务员需要查询流程完结的申请单,我们就可以通过“AND”组合不同的规格进行实现。
7.2.4 归纳抽象对于有定语修饰的名词,要注意分辨它们是类型的差异,还是值的差异。如配送地址和家庭地址,订单状态和商品状态。如果是值的差异,类型相同,应归并为一个领域概念(如,配送地址和家庭地址);而类型不同,则不能合并(如,订单状态和商品状态)。
特别地,当定语修饰的名词中,定语表示的是不同的限界上下文,且名词相同时(即名称相同、含义不同的领域概念),我们应该尽可能调整命名,确保含义不同的领域概念的名称不同,以避免不必要的歧义和沟通上的误解。比如:商品的订单和库存的订单在特定限界上下文内都可以命名为order,但是如果把库存的订单改为库存的配送单delivery效果会更好。
7.2.5 确认关系根据业务需求和领域知识,判断领域概念之间是否存在关联。且对于1:N, N:1, M:N的关联关系,我们需要判断是否可以为这些关联关系定义一个新的类型,比如作品与读者存在1:N的关系,我们可以定义“订阅”这个概念来描述这种关系。
注意,我们需要尽量避免对象中的双向关系,即对象A关联对象B,而对象B关联对象A。当两个对象存在双向关系时,会为管理他们的生命周期带来额外的复杂度。我们应该规定一个遍历方向,来表明一个方向的关联比另一个方向的关联更有意义且更重要,比如示例SMS中,成绩会关联课程(成绩实例中包含课程ID),而课程不会关联成绩。当然,当双向关系是领域的一个概念时,我们还是应该保留它。
7.2.6 示例-SMS的领域分析模型通过名词建模,动词建模和归纳抽象后,可提炼出以下领域对象:成绩(Result)、绩点(gpa)、总成绩(total result)、总绩点(total gpa)、学年(school year)、学期(semester)、课程(course)、学分(credit)、申请单(application receipt),邮件(mail),排名(rank),申请单状态(application receipt status)
这些领域对象之间的关系如下图所示。
7.3 领域设计建模领域设计建模的核心工作就是设计聚合和设计服务,在这之前我们需要先了解一下设计要素(实体、值对象、聚合、工厂、资源库、领域服务、领域事件)。
7.3.1 设计要素领域驱动设计强调以“领域”为核心驱动力。设计领域模型时应该尽量避免陷入到技术实现的细节约束中。但很多时候我们又不得不去思考一些非领域相关的问题:
- 领域模型对象在身份上是否存在明确的差别?
- 领域模型对象的加载以及对象间的关系如何处理?
- 领域模型对象如何实现数据的持久化?
- 领域模型对象彼此之间如何做到弱依赖地完成状态的变更通知?
为了解答上述的四个问题,DDD提供了很多的设计要素,它们能够帮助我们在不陷入到具体技术细节的情况下进行领域模型的设计。
7.3.1.1 实体
实体的核心三要素:身份标识、属性和领域行为。
身份标识:身份标识的主要目的是管理实体的生命周期。身份标识可分为:通用类型和领域类型。通用类型ID没有业务含义;而领域类型ID则组装了业务逻辑,建议使用值对象作为领域类型ID。
属性:实体的属性用来说明主体的静态特征,并持有数据与状态。属性分为:原子属性和组合属性。组合属性可以是实体,也可以是值对象,取决于该属性是否需要身份标识。我们应该尽可能将实体的属性定义为组合属性,以便于在实体内部形成各自的抽象层次。
领域行为:体现了实体的动态特征。实体具有的领域行为一般可以分为:
- 变更状态的领域行为:变更状态的领域行为体现的是实体/值对象内部的状态转移,对应的方法入参为期望变更的状态。(有入参,无出参);
- 自给自足的领域行为:自给自足意味着实体对象只操作了自己的属性,不外求于别的对象。(无入参);
- 互为协作的领域行为:需要调用者提供必要的信息。(有入参,有出参);
- 创建行为:代表了对象在内存的从无到有。创建行为由构造函数履行,但对于创建行为较为复杂或需要表达领域语义时,我们可以在实体中定义简单工厂方法,或使用专门的工厂类进行创建。(有出参,且出参为特定实体实例)。
7.3.1.2 值对象
一个领域概念到底该用值对象还是实体类型,判断依据:
- 业务的参与者对它的相等判断是依据值还是依据身份标识;
- 确定对象的属性值是否会发生变化,如果变化了,究竟是产生一个完全不同的对象,还是维持相同的身份标识;
- 生命周期的管理。值对象无需进行生命周期管理。
值对象具有不变性。值对象完成创建后,其属性和状态就不应该再进行变更了,如果需要更新值对象,则通过创建新的值对象进行替换。
由于值对象的属性是在其创建的时候就完成传入的,那么值对象所具有的领域行为大部分情况下都是“自给自足的领域行为”,即入参为空。这些领域行为一般提供以下的能力。
- 自我验证:验证传入值对象的外部数据是否正确,一般在创建该值对象时进行验证。
- 自我组合:当值对象涉及到数值运算时,可以定义相同类型值对象的方法,使值对象具有自我组合能力。比如示例-SMS中,在统计成绩时会涉及学分相加的运算,因此我们可以将相加运算定义为可组合的方法,便于调用者使用。