Skip to content

20 值对象(下):值对象和实体的本质区别是什么?

你好,我是钟敬。

在前两节课,我们学习了 值对象 的基本概念、编程实现以及值对象的优点,基本上已经可以开始在实践中应用了。现在我们已经知道, 实体 是靠独立于其他属性的标识来确定同一性的,而 值对象 以本身的值来确定同一性,没有独立于其他属性的标识;理论上,实体是可变的,而值对象是不可变的。

但是,还有两个问题没有彻底解决:一个问题是怎么在领域模型里表示值对象;另一个问题更深一点,那就是,实体和值对象有这种区别的根源是什么?如果不理解这一点,在遇到一些疑难问题的时候,仍然无法分辨。

这节课,我们就来解决这两个问题。开始之前,我先提示一下,后面有不少图片和代码,你可以边看文稿边听我说。

UML的两个知识点

为了讲清楚怎么在模型里表示值对象,我们先要讨论UML里的两个知识点。

属性和关联的等价性

第一个知识点是“属性(attribute)和关联(association)的等价性”。这在UML里是一个比较深入的原理。

什么意思呢?咱们先来看看后面这个简单的模型图。

这个图的意思是, 员工 作为 组织 成员的时候, 组织员工 之间是一对多的关联。其实这种关联还有另一种画法,虽然形式不同,但意思是一样的。可以看看下面这张图。

看完以后,你是不是觉得有点奇怪。表示关联的线跑哪儿去了?

首先我们仔细看看 员工 这个类型。里面的属性写作“直属组织: 组织[1..1]”,冒号前面的 直属组织 是属性的名称,冒号后面的 组织 是这个属性的类型,也就是上面画出来的那个方框所代表的 组织 类型,我把这个对应关系用蓝色的字标出来了。

类型名后面的中括号就是这个属性的“多重性”,这里的[1..1]就代表员工至少属于一个 直属组织,最多也只属于一个 直属组织。你应该发现了,这个 直属组织 属性,其实就代表了原来图里的那根“线”。从 组织 类型那边看,道理也是一样的。

下面我们把这两个图并列起来,方便你进一步理解它们的对应关系。

这个图的左边是按照“关联”的方式画的,而右边是按照“属性”的方式画的。红色的线表示等价关系。

我们来梳理一下。

第一,左边员工充当的 成员 角色,等价于右边组织类型里的 成员 属性。

第二,左边的 员工 类型,等价于右边 成员 属性的 员工 类型。

第三,左边的多重性 “0..*”,等价于右边 成员 属性的 员工 类型后面的 “[0..*]”

另外,右边的 组织 类型的 成员 属性,或者 员工 类型的 直属组织 属性,都隐含着左图里的关联线,但没有画出来。

在上面的图里,左图用一条表示关联的线来表达 组织员工 之间的关系,右图则用 属性 的方式来表达,两者的含义是等价的,这就是 关联和属性的等价性

顺便说一个建模初学者常见的错误,我们看看下面这张图。

发现问题在哪了吗? 直属组织 这个属性本身,就意味着 组织员工 之间隐含着关联线。因此两者表达的意思发生了重复,而一张图里不应该有这样的重复。所以,要么保留属性,删掉关联,要么保留关联,删掉属性,但两者不能同时出现。

属性要素的省略

讲完了属性和关联的等价性,我们再说UML的第二个知识点——属性要素的省略。

UML里的很多元素都是可以省略的,关键看你想强调什么。总的原则是, 在表意清楚的前提下,尽量简洁。为了讲值对象的建模,我们今天主要讨论和属性有关的要素的省略。

还是拿 员工 类型来举例吧,我们看看它的 直属组织 属性。

之前我们建模的时候好像没写这么复杂,一般只写了属性名,这是因为其他部分被省略了。

在这个例子里,如果我们觉得“一个员工肯定只属于一个组织,不可能属于两个”这一点很显然,根本不必强调的话,那么就可以省掉多重性,变成下面的样子。

如果我们还觉得 直属组织 的类型显然就是 组织,没必要强调,那么可以再省去类型的说明。像下面这样。

反之,如果我们更想强调, 员工 有一个类型为 组织 的属性,至于这个属性叫什么名字反而不重要,那么就可以省掉属性名,只保留类型,变成下面这样。

如果我们觉得这个属性根本不重要,即使省去也不会影响我们理解整个业务知识,那么可以省掉整个属性的描述,画成下图里的样子。

现在,你已经理解了属性要素的省略方法了吧。总之,核心思想就是,属性的作用是表达领域知识,我们应该省略掉显而易见或者不关注的内容,标识出需要强调的部分。

如何在模型里表示值对象

好了,我们再回过头,思考一下关联和属性的等价性。你是不是已经在思考一个问题:既然在画领域模型的时候可以采用两种方式,那么我们该怎么选择呢?下面我们通过分析值对象在模型里的表示方式,把这个问题讲清楚。

值对象的基本表示方法

第18课,我们重点讲过两个值对象,一个是 时间段,一个是 员工状态,我们还是沿用这两个例子。

首先聊聊 时间段 应该怎么在模型里表示。先看看我们之前画的 时间段 类型。

我们首先给这个类加一个衍型,来说明这是值对象。

《DDD》原书里并没有给出表示值对象的衍型。用<>这个衍型来表示值对象,是 Martin Fowler的习惯,我们沿用了这个做法。那么,需不需要用<> 衍型来标识实体呢?

这样做当然也没错,但是一般来说没必要。这是因为除了值对象,就是实体,所以我们把值对象标识出来,就足以区分两者了。

工作经验 这个实体有一个 时间段 属性,原来我们画成了下面这样。

这种画法是没有问题的。这里用了属性而不是关联的方式,来表达 工作经验时间段 的关系,并且在属性后面省略了 时间段 的类型。

如果把整个属性写全,就是后面这样。

这里的属性名和类型名恰好同名,但表达的是不同的概念,要注意区分。

那么问题又来了,请你想一下,这里可不可以用关联的形式来表达呢?如果用关联的形式,就会是下面的样子。

注意, 时间段工作经验 是一对多关联。也就是说,一个 工作经验 只有一个 时间段,而一个 时间段,可以用于很多人的 工作经验

另外你可能已经注意到, 时间段 里用到的 日期 其实也是值对象,如果再展开一级的话,就成了下面这样。

这种画法虽然含义是对的,但你是不是在直觉上已经觉得,没有必要做这样的展开呢?如果我们把一张图上的所有值对象都这样展开,这张图就会变得非常复杂,重点也不突出,那就很难理解了。

一般而言,我们在领域知识里主要考虑的还是实体和实体之间的关系。而值对象多数是用来说明实体的属性的,没有必要关注一个值对象和使用它的实体之间到底是一对一还是一对多。所以我的建议是, 一般情况下,实体之间的关系用关联来表达,而实体和值对象之间的关系用属性来表达

在实践的时候,我常常发现有些小伙伴不太容易理解值对象和实体之间的一对多关系。尽管在实际和业务人员建模的时候,我们一般不需要体现出这种关系,但是从学习的角度,我们自己心里还是应该充分理解的。所以,在自己练习的时候,我倒是建议你试试把值对象展开成关联的方式,比较一下它和属性方式的区别。这是因为用了关联方式以后,这种一对多的关系才能充分展现出来。

枚举型值对象

接下来,我们看另一个值对象——员工状态。按照前面的方法,我们可以把 员工员工状态 画成下面的样子。

回忆一下我们在第18课里讲过的值对象分类,可以发现 员工状态时间段 有两个区别。第一, 员工状态 是预定义的, 时间段 是非预定义的;第二, 员工状态 是依附于实体的,而 时间段 是独立的。

我们先说说第一点, 员工状态 是预定义的。上图里的注释,就说明了预定义的三种员工状态。这种属性,也称为 可枚举的。在UML里有一个表示枚举类型的衍型<>,用了这个衍型以后, 员工状态 就可以像下面这样表示。

对于枚举类型,符号的下面那栏里就不是属性了,而是可供选择的枚举值。

值对象放在哪个包

我们再来看第二点区别:员工状态是 依附于实体的,而时间段是 独立的。这个差别,影响的是模块或者聚合包的划分。明白了这一点,我们就可以画出下面这张图。

由于 员工状态 是依附于 员工 实体的,所以放在表示 员工 聚合的包里是合适的。 时间段 虽然被 工作经验 使用,但是并不局限于 工作经验。 相反,它是公共的,可以被很多不同的实体使用,所以放在一个公共包里。

值对象的省略

还有一种情况,如果我们不是刻意要强调 员工状态 这个类型,也可以干脆省略掉不画,并且可以把枚举值的选项直接写在实体的属性后面。像下面这样。

员工状态 属性后面的大括号,我们之前学过,表示“约束”。只不过原来的约束是写在注解里,现在直接附在属性后面。竖杠(|)表示“或”的关系。也就是说,我们对 员工状态 做了约束,它只能是试用期、正式工、终止这三种状态中的一种。这种方式会更简洁一些。

值对象和实体的本质区别

好,现在我们终于可以来回答课程开头提的那个问题了:实体和值对象之间的区别的本质和根源到底是什么?

之所以这个问题有点难,是因为,我们必须从人类认识事物的方式这个角度,才能把这个问题说清楚。也就是说,有点上升到哲学层面了。

实体一般是我们能够感受到的客观存在的外部事物。比如说一张桌子,人看到,是一张桌子。狗看到,也是一张桌子,虽然狗不知道这个东西叫“桌子”。从这个角度来说,对实体(至少是自然界里的实体)的直觉认识能力,是人和动物共有的。

也有一些实体是人想象出来的,例如仙女。尽管仙女很可能不存在,但她是以实际存在的事物为蓝本想象出来的,在人心中的感受是类似的。还有一些自然界不存在的事物,比如“公司”、“合同”等等。这些是人类社会关系的产物,但它们在人头脑中的感受也与自然实体类似。

值对象就不一样了。我们从5张桌子、5只山羊、5棵树这些事物里,抽象出“5”这个概念,用于描述数量。“5”是整数,整数是值对象的类型,“5”是值对象的实例。

我们又发明了字母,并约定用字符串“table”来指称“桌子”这个概念。“字符串”是值对象的类型,“table”是字符串类型的一个实例。

所以,值对象是人类为了认识和描述事物的属性,在头脑里经过抽象思维创造出来的概念。这些概念在自然界中是不存在的。只有智慧生物才有这种抽象能力。狗的头脑里是不会有“5”和“table”的概念的。

这种认识论上的区别也得到了科学的佐证。生物学家巴浦洛夫提出了“第一信号系统”和“第二信号系统”学说。这个学说认为,大脑皮质最基本的活动是信号活动。信号可以分成两类:一类是现实的、具体的刺激,比如声、光、热等等,称为第一信号;另一类是抽象的刺激,比如说语言和数字,称为第二信号。

对第一信号发生反应的大脑机能,叫做第一信号系统,是动物和人共有的。对第二信号发生反应的机能,叫作第二信号系统,是人类所特有的。联系到DDD,大体上可以说,对实体的认识主要是通过第一信号系统,而对值对象的认识则是通过第二信号系统。

现实中的事物,也就是实体,总有一个产生和消亡的过程,在这个过程里,各种属性也可能发生变化,因此是可变的。

而值对象则是纯粹的概念产物,唯一的目的就是方便人的思考和沟通。所以,这样的概念本身并没有自然的产生和消亡过程,也不需要改变。5 就是 5 , 5 如果变成 6 ,那么就已经是另一个值对象了,原来的 5 还在那里,并没有改变。换句话说,值对象并没有实体意义上的“生命周期”。因此,谈论值对象的改变,本身是没有意义的,这就是值对象不变性的本质原因。

总结

好,这节课的主要内容就讲完了,下面来总结一下。

在UML里,属性和关联具有等价性。也就是说,两个领域对象之间的关系,既可以用关联的方式表达,也可以用属性的方式来表达。两种方式从涵义上是等价的,但是对于同一个关系,不能同时用两种方式。

而在实践中,由于我们往往更关心的是实体之间的关联关系,所以一般建议,在领域模型图里, 实体之间的关系用关联来表达,而实体和值对象之间的关系用属性来表达。不过,在自己练习的时候,用关联的方式画一画值对象,有助于你的深入理解。

值对象本身,可以用<>衍型来标识。对于可枚举的值对象,还可以采用UML专门的枚举衍型来表达。对于依附于实体的值对象,可以放在实体所属的聚合包里,而不依赖于实体的值对象,可放在公共包里。如果某些值对象不需要强调,那就不必专门画出来。

实体和值对象的本质区别在于,实体是人通过感官可以感觉到的客观存在的事物,或者以存在的事物为蓝本想象出来的事物;而值对象是为了描述事物,由人抽象出来的纯粹概念。讨论值对象的变化是没有意义的。

思考题

我给你留了两道思考题。

1.有人认为值对象就是DTO(数据传输对象),你同意吗,为什么?

2.你觉得和业务人员谈需求的时候,需要强调值对象这个概念吗?

好,今天的课程结束了,有什么问题欢迎在评论区留言。下节课,我们会讲一种能丰富领域知识,并且化简关联关系的建模技术——限定。