实体和值对象
消息场景:用户 A 发送一个消息给用户 B,用户 B 回复一个消息给用户 A。。。
现有设计:消息设计为实体并为聚合根,发件人、收件人设计为值对象。
三个问题:
- 实体最重要的特性是什么?
- Message 实体是怎么得来的?
- 发件人、收件人为什么不是实体?
1. 实体最重要的特性是什么?
《领域驱动设计》5.2 实体:
摘录一段:许多对象不是由它们的属性来定义,而是通过一系列的连续性(continuity)和标识(identity)来从根本上定义的。
归纳:
- 标识(identity)
- 连续性(continuity)
标识在实体中的另一种体现就是唯一和不可变,其概念在很多资料中有说明,这也是实体最重要的特性。
我有一个双胞胎哥哥,我们俩出生的时候,长得一模一样,以至于我们的爸妈都分不清,不得已他们在我们脖子上系个项链来标记:谁是老大?谁是老二?其实这个“标记”就可以看作是实体的标识,只不过是用项链来标识的,就像我们在项目中使用 GUID 方式一样,目的就是用来体现标识,但不管用什么方式表示,这个标识必须在这个特定环境下唯一,也就是说,我和我双胞胎哥哥的项链不能完全一样,要不然我爸妈就不能区分我们俩了。
我和我那双胞胎哥哥就这样一天一天的长大,但出奇的是,我们哥俩越长越像,以至于我们互相看对方,都以为自己在“照镜子”一样,但唯一不变的是我们俩脖子上的项链,这也是区分我们哥俩的唯一方式。刚出生的我和现在的我,脖子上的项链是一样的,这也就是实体的不可变性,也就是说刚出生的我和现在的我是同一个人,项链只不过在我成长的过程中起到“标记”的作用(当然也可以是手带、脚环之类的信物),它会“陪伴”我的一生,这个过程,可以理解为实体的另一种特性-连续性。
有一天,我们镇要统计双胞胎的分布情况,然后调查人员来到我们家,问我们爸妈:“你们家里有没有双胞胎?几对双胞胎?龙凤胎?还是。。。”,然后我爸妈就报上:“一对双胞胎-两个小子”,然后调查人员就做了笔记走了。在这个过程中,他们丝毫没有提及我脖子上的“项链”,虽然它在我爸妈眼里是那么重要(用来标记我们哥俩),但在调查人员眼里却什么都不是,他们只需要知道我和我双胞胎哥哥是什么样的双胞胎就行了,这也就是实体和值对象的根本区别:实体不仅需要知道它是什么?而且还需要知道它是哪个?而值对象只需要知道它是什么?
特定环境下,实体和值对象的区分例子有很多,比如《领域驱动设计》书中所说的“体育场座位例子”和“ Custorm-Address 例子”等等,但大部分都是强调实体的标识特性,却很少提及连续性,那什么是连续性?这部分内容,在《领域驱动设计》中5.2实体章节中最后部分有提及,但都是零碎的概念性文字,如果不注意的话,很容易会被忽略掉。
摘录一段:只要一个对象在生命周期中能够保持连续性,并且独立于它的属性(即使这些属性对系统用户非常重要),那它就是一个实体。
这个内容可以结合上面我和我双胞胎哥哥的例子进行理解,“项链”会陪伴的我一生,这段话可以拆分对应理解:项链-标识、一生-生命周期、陪伴-连续性。也就是说连续性不能理解为生命周期,它应该理解为:标识在实体生命周期内体现出连续性。
2. Message 实体是怎么得来的?
结合上面实体特性的理解,Message 实体是怎么得来的就很好理解了,消息场景毫无疑问聚合的是消息,消息实体是怎么得来的?可以换个角度理解:为什么把消息设计为实体?首先看下消息实体符不符合实体的两个特性。
-
标识(identity):消息场景中消息的区分通过什么?标题?内容?这些都不行,为了保证消息的唯一性,必须使用标识进行区分,而且必须不可变。消息场景中,有可能会出现标题和内容一样的消息,但这却不是同一个消息,就像我和我那双胞胎哥哥,长的一样,却不是同一人,可以这样说:标识的作用就是为了区分,而消息也必须要区分,所以。。。
-
连续性(continuity):一次我们家吃饭的时候,我一不小心把饭碗给打碎了,然后我妈就痛打了我一顿,她有个做笔记的习惯,记录我们哥俩的日常生活,比如这次需要记录一下:今天打了谁?但当时她打完我之后,却不记得是打了我?还是我哥?然后她就挨个看我们的屁股和项链,来确定今天打了谁?这就是标识在生命周期中连续性的体现。消息场景中,在某一阶段需要对消息进行处理,这个处理需要通过标识来明确处理的是哪条消息?这个对消息处理过程的体现就是连续性,有时候连续性需要在标识明确的情况下,但还有一种是其自身的生命周期连续性,比如从消息的创建,到管理,再到最后的销毁,这个过程就是消息实体的连续。
上面的分析说明消息实体符合实体的两个特性,也就是说消息可以设计为实体,至于怎么得来的?可以这样理解,消息场景首先考虑的是消息,就像我们家的双胞胎,首先考虑的是我和我那双胞胎哥哥。
3. 发件人、收件人为什么不是实体?
在之前的一篇博文中,园友鼻涕成诗有这样的疑问:联系人作为值对象这一点有点不太理解,好处是什么?我当时是这样回复的:
联系人作为值对象,因为他不在消息系统中存储,是从外部获取的,而且它的存在要依附于消息,在消息系统这个业务场景中,如果脱离了消息,它就没有什么意义,对于消息而言,我只要知道这个联系人的内容是什么就行了,而不需要它具体什么哪个,人?还是邮箱?这个它并不关心,不是说把联系人作为值对象有什么好处,而是在这个业务场景下,这样设计比较合理些。
回复内容现在看来有些牵强,先不讨论对与错,按照上面消息实体的分析模式,在消息场景下,看下发件人、收件人(可以统称为联系人,发件人和收件人有可能为同一联系人)是否具有实体的一些特性。
-
标识(identity):联系人是否具有标识?也就是说联系人需不需要进行区分?答案当然是要进行区分,要不然收件箱、发件箱就没办法针对收件人、发件人进行标识,而且联系人有可能名称相同,但是两个不同的联系人,也就是说在消息的整个应用场景中,联系人是必须要唯一标识的,不管它扮演的角色是发件人,还是收件人,这个“角色扮演”概念只是针对某一具体消息来说,联系人所存在的意义(在这个消息中,这个联系人是发件人,但在另外一个消息中,有可能是收件人),但相对于整个消息场景,这个联系人是唯一的,而且是不可变的。
-
连续性(continuity):这个可能没有消息实体的连续性好理解,联系人的连续性其实是依附于消息实体而言,它如果独立出来,自身在消息场景中,是没有连续性概念的,就比如在创建消息的时候,我需要判断收件人是否存在,存在的话就创建收件人对象,并赋予创建消息的收件人属性,还有就是消息在被阅读的时候,需要判断阅读人是否有阅读权限等等,这一些操作,就体现出联系人的连续性依附于消息实体,但不可否认,联系人的创建、使用、舍弃操作等,都可以理解围绕某一具体消息的生命周期,也就是联系人的连续性,而且在这个过程中,联系人的标识都需要首先被明确。
在之前的理解中,联系人设计为值对象的想法是,把联系人看作是一个值,一个依附于消息实体的具体值,我只需要知道这个值就行了,具体体现就是 SenderID 或 RecipientID,其实这个就是联系人的标识,只是当时被两点所迷惑:
- 联系人外部存储:在消息场景中,联系人的获取是从外部获得的,也就是说联系人不在消息场景中存储,也不进行管理,只是一个获取操作,这个和一般的实体场景不太一样,但仔细一想,不管它是从哪里获取的,这个不应该在消息场景中所关心,我应该专注于联系人在消息场景中的连续性。
- 联系人依附于消息:这个是最重要的迷惑点,或者说是我根本不了解实体和值对象到底应该是什么?联系人独立于消息,在消息场景中,没有任何意义,但不能因为这一点,就把它设计为值对象,有很多实体是依附关系,只要它存在标识和连续性,那它就是实体。
把联系人设计为值对象当然也有“好处”,比如可以减少对联系人的管理,因为如果联系人设计为值对象,那它就是一个值,也就没有对象的概念,但出来混的迟早是要还的,我要加一个用户禁言功能,这个在现有的设计中就不好进行实现。像这种依附性实体的场景也很多,比如购物车应用中的 Order 和 Custorm,Custorm 依附于 Order,这个首先明确的是购物车应用场景,如果是其他的场景下,那 Custorm 就不存在依附关系。
我和我双胞胎哥哥出生的时候,在我们的保温箱上,除了需要标明我们两个的”身份“之外,还需要标明我们爸妈的”身份“,具体标识可以用身份证号,这个就想消息实体中的 SenderID、RecipientID 一样,虽然它是一个”值“,但我还需要知道它具体标识的是哪个对象,因为我不仅需要它表示的值是多少,我还需要知道它所代表的对象是哪个,就比如我和我双胞胎哥哥要根据这个身份证号,找到我们的父母一样。
4. 发件人、收件人是值对象?还是实体?
话不言多,总之一句话:发件人、收件人(联系人)需要设计为实体。
消息场景实体和值对象:
- Message 消息实体和 Contact 联系人实体。
- 值对象若干(如 MessageState、MessageType 等)。
概念理解: