1.实体

1.1 为什么:现实到代码模型的映射

1.2 是什么: 具有唯一标识拥有自己的行为与属性主体对象

一个典型的实体应该具备三个要素:

  • 身份标识
  • 属性
  • 领域行为

    身份标识

    身份标识(Identity,或简称为 ID)是实体对象的必要标志,换言之,没有身份标识的领域对象就不是实体。实体的身份标识就好像每个公民的身份证号,用以判断相同类型的不同对象是否代表同一个实体。身份标识除了帮助我们识别实体的同一性之外,主要的目的还是为了管理实体的生命周期。实体的状态是可以变更的,这意味着我们不能根据实体的属性值进行判断,如果没有唯一的身份标识,就无法跟踪实体的状态变更,也就无法正确地保证实体从创建、更改到消亡的生命过程。
    一些实体只要求身份标识具有唯一性即可,如评论实体、博客实体或文章实体的身份标识,都可以使用自动增长的 Long 类型或者随机数与 UUID、GUID,这样的身份标识并没有任何业务含义。有些实体的身份标识则规定了一定的组合规则,例如公民实体、员工实体与订单实体的身份标识就不是随意生成的。遵循业务规则生成的身份标识体现了领域概念,例如公民实体的身份标识其实就是“身份证号”这一领域概念。定义规则的好处在于我们可以通过解析身份标识获取有用的领域信息,例如解析身份证号,可以直接获得该公民的部分基础信息,如籍贯、出生日期、性别等,解析订单号即可获知该订单的下单渠道、支付渠道、业务类型与下单日期等。
    在设计实体的身份标识时,通常可以将身份标识的类型分为两个层次:通用类型与领域类型。
    通用类型提供了系统所需的各种生成唯一标识的类型,如基于规则的标识、基于随机数的标识、支持分布式环境唯一性的标识等。这些类型都将放在系统层代码模型的 domain 包中,可以作为整个系统的共享内核。

领域类型的身份标识往往具备领域知识和业务逻辑。它是实体的身份标识,但它自身却应该被定义为值类型,保持值的不变性,同时提供属于身份标识的常用方法,隐藏生成身份标识值的细节,以便于应对未来可能的变化。

属性

实体的属性用来说明其主体的静态特征,并通过这些属性持有数据与状态。通常,我们会依据粒度的大小将属性分为基本属性组合属性。简单说来,定义为开发语言内置类型的属性就是所谓的“基本属性”,如整型、布尔型、字符串类型等;与之相反,组合属性则通过自定义类型来表现。

这两种属性之间是否存在什么分界线?例如说,难道我们就不能将 category 定义为 String 类型,将 weight 定义为 Double 类型吗?又或者,难道我们不能将 name 定义为 Name 类型,将 quantity 定义为 Quantity 类型吗?我认为,划定这条边界线的标准是:该属性是否存在约束规则、组合因子或属于自己的领域行为?
先来看约束规则。相较于产品名而言,产品的类别具有更强的约束性,避免出现分类无休止地增长,过多离散细小的分类反而不利于产品的管理。更何况,从业务规则来看,产品的类别可能还存在一个复杂的层次结构,单单靠一个字符串是没法表达如此丰富的约束条件与层次结构的。当然,如果需求对产品名也有明确的约束,为其定义一个 Name 类也未尝不可;只是相比较而言,定义 Category 的必要性更有说服力罢了。

设计实体时,应该遵循保持实体专注于身份这一设计原则,让实体只承担符合它身份的业务行为,而把内聚性更强的属性分解为单独的值对象,并运用“信息专家模式”将操作了值对象属性的业务行为推向值对象,让值对象成为高内聚的体现领域逻辑的对象。这样的设计符合面向对象设计思想的“职责分治”原则,即依据各自持有的数据与状态以及和领域概念之间的粘度来分配职责,保证了实体类的单一职责。

领域行为

实体拥有领域行为,可以更好地说明其主体的动态特征。一个不具备动态特征的对象,是一个哑对象,一个蠢对象。这样的对象明明坐拥宝山(自己的属性)而不自知,反而去求助他人帮他操作自己的状态,不是愚蠢是什么?为实体定义表达领域行为的方法,与前面讲到组合属性需要封装自己的领域行为是一脉相承的,都是“职责分治”的设计思想体现。
实体的领域行为依据不同的特征,可以分为:

  • 变更状态的领域行为
  • 自给自足的领域行为
  • 互为协作的领域行为

    变更状态的领域行为

    一个实体对象的状态是由属性持有的,与值对象不同,实体对象是允许调用者更改其状态的。许多语言都支持通过 get 与 set 方法(或类似的语法糖)来访问状态。然而,领域驱动设计强调代码模型也是领域模型的一部分,因此代码模型中的类名、方法名都需要以业务角度去表达领域逻辑,甚至希望领域专家也能够参与到编程元素的命名讨论上。至少,我们应该让这些命名遵循团队共同制定的统一语言。因此,单从命名看,我们并不希望遵循 Java Bean 的规范,单纯地将这些变更状态的方法定义为 set 方法。例如,修改产品价格的领域行为就应该定义为 changePriceTo(newPrice) 方法,而非 setPrice(newPrice):

自给自足的领域行为

既然是自给自足,就意味着实体对象只能操作自己的属性,而不外求于别的对象。这种领域行为最容易管理,因为它不会和别的实体对象产生依赖。即使发生了变化,只要定义好的接口无需调整,就不会将变化传递出去。例如,航班实体对象 Flight 定义了计划飞行时间、估算飞行时间与实际飞行时间等属性,领域逻辑需要获得这三者之间的统计值:

互为协作的领域行为

除了操作属于自己的属性,实体也可以调用别的对象,形成一种协作关系。要注意区分实体属性与外部对象。如果实体对象操作的是自己的属性对象,就不属于互相协作的范畴。因此,参与协作的对象通常作为方法的外部参数传入。例如,在 Rental 实体中,如果需要根据客户类型计算每月的租金,就需要与 CustomerType 对象进行协作:

  1. public class Rental extends Entity<RentalId> {
  2. public Price monthlyAmountFor(CustomerType customerType) {
  3. if (customerType.isVip()) {
  4. return this.amount.multiple(1 - DISCOUNT);
  5. } return this.amount;
  6. }
  7. }

对象之间若要默契配合,形成良好的协作关系,就需要通过行为进行协作,而不是让参与协作的对象成为数据的提供者。”这要求参与协作的对象皆为操作自身信息的自治对象,无论是实体、值对象,都是各自履行自己的职责,然后基于业务场景进行行为上的协作。

在领域逻辑中,还有一种特殊的领域行为,就是针对实体(包括值对象)进行增删改查的操作,分别对应增加、删除、修改与查询这四个操作。从对象的角度考虑,这四个操作其实都是对对象生命周期的管理。如果我们将创建的对象放到一个资源库(Repository)中进行管理,则增删改查操作其实就是访问资源库。在领域驱动设计中,针对实体的增删改查操作都分配给了专门的资源库对象。换言之,在领域驱动的设计模型中,实体往往并不承担增删改查的职责。“变更状态的领域行为”,仅仅针对对象的内存状态进行修改。
除此之外,还有创建行为。领域驱动设计引入了工厂类封装复杂的创建行为,有时候,也可能由实体类扮演工厂角色,提供创建实体对象的能力。无论是增删改查,还是对象的创建,都属于一个对象的生命周期。

1.3 怎么做:

2.值对象

2.1 为什么:简化从现实到代码模型的映射

2.2 是什么:依附于实体的不具有唯一标识的拥有自己的行为与属性的对象

值对象通常作为实体的属性而存在,也就是亚里士多德提到的数量性质关系地点时间形态等范畴。正如 Eric Evans 所说:“当你只关心某个对象的属性时,该对象便可做为一个值对象。为其添加有意义的属性,并赋予它相应的行为。我们需要将值对象看成不变对象,不要给它任何身份标识,还有要尽量避免像实体对象一样的复杂性。”
在进行领域驱动设计时,我们应该优先考虑使用值对象来建模而不是实体对象。因为值对象没有唯一标识,于是我们卸下了管理身份标识的负担;因为值对象是不变的,所以它是线程安全的,不用考虑并发访问带来的问题。值对象比实体更容易维护,更容易测试,更容易优化,也更容易使用,因此在建模时,值对象才是我们的第一选择。

2.3 怎么做:

值对象要自给自足的验证

3.值对象与实体的本质区别

当我们无法分辨表达一个领域概念该用实体还是值对象时,就可以问一下自己:你是用对象的属性值判等,还是用对象的身份标识来判等? 例如,在销售系统中,一个客户(Customer)可以有多个联系人(Contact),它们应该被定义成实体还是值对象?从判等的角度看,当两个客户的属性值都相等时,可以认为他(她)们是同一个客户吗?从领域合规性来看,显然不能,至少这种判等方式可能存在偏差。从业务逻辑看,我们往往更关注不同客户的身份标识,以此来确定他(她)是否我们的客户!对于联系人而言,当一个客户提供了多个联系人信息时,就可以仅通过联系信息如电话号码来判别是否同一个联系人。因此,客户是实体,联系人是值对象

在针对不同领域、不同限界上下文进行领域建模时,注意不要被看似相同的领域概念误导,以为概念相同就要遵循相同的设计。任何设计都不能脱离具体业务的上下文。例如钞票 Money,在多数领域中,我们都只需要关心它的面值与货币单位。如果都是人民币,面值都为 100,则此 100 元与彼 100 元并没有任何实质上的区别,可以认为其值相等,定义为值对象类型。然而,在印钞厂的生产领域,管理者关心的就不仅仅是每张钞票的面值和货币单位,而是要区分每张钞票的具体身份,即印在钞票上的唯一标识。此时,钞票 Money 就应该被定义为实体类型。

总而言之,是否拥有唯一的身份标识才是实体与值对象的根本区别。正是因为实体拥有身份标识,才能够让资源库更好地管理和控制它的生命周期;正是因为值对象没有身份标识,我们才不能直接管理值对象,使得它成为了实体的附庸,用以表达主体对象的属性。至于值对象的不变性,则主要是从优化、测试、并发访问等非业务因素去考量的,并非领域设计建模的领域需求