实体和值对象, entity, value object
Contents
实体和值对象, entity, value object
实体和值对象 实体和值对象放在一起讲容易区分,概括而言,实体不仅需要知道它是什么?而且还需要知道它是哪个?而值对象只需要知道它是什么?先看定义:
实体
许多对象不是由它们的属性来定义,而是通过一系列的连续性(continuity)和标识(identity)来从根本上定义的。只要一个对象在生命周期中能够保持连续性,并且独立于它的属性(即使这些属性对系统用户非常重要),那它就是一个实体。 值对象:当你只关心某个对象的属性时,该对象便可作为一个值对象。为其添加有意义的属性,并赋予它相应的行为。我们需要将值对象看成不变对象,不要给它任何身份标识,还应该尽量避免像实体对象一样的复杂性。 对于实体Entity,实体核心是用唯一的标识符来定义,而不是通过属性来定义。即即使属性完全相同也可能是两个不同的对象。同时实体本身有状态的,实体又演进的生命周期,实体本身会体现出相关的业务行为,业务行为会实体属性或状态造成影响和改变。
如果从值对象本身无状态,不可变,并且不分配具体的标识层面来看。那么值对象可以仅仅理解为实际的Entity对象的一个属性结合而已。该值对象附属在一个实际的实体对象上面。值对象本身不存在一个独立的生命周期,也一般不会产生独立的行为。
初看还是很难理解,举几个例子:
案例分析 营业厅会卖手机以及很多手机配件,在客户业务规则中,往往每一部手机都要单独管理,通过手机的SN号来识别。而手机配件是一种数量类型的实物,只关心其数量的变化,并不关心到每一个具体的手机配件。这种场景就是典型的实体和对象的案例。 地址是实体还是值对象。在电力公司服务软件中,一个地址对应于公司线路和服务的目的地。如果多个住所都申请了电力服务,那么这个公司需要知道这一点,因此地址是实体。我们也可以用另一种方法,在模型中将“住所”关联到运营服务,其中“住所”是一个包含地址属性的实体。此时,地址就是一个值对象。 体育场座位例子。当我们发放的门票上有座位号的时候,座位需要作为独立的实体,座位号是唯一的标识。而当先到先座模式下,我们只关心剩余座位数,那么座位号并不是唯一标识,这时候座位就可以作为一个值对象。这跟我们的业务需求有关。 消息场景中,发件人、收件人是实体?还是值对象?这个在 三个问题思考实体和值对象一文中有讨论。 值对象的常见例子包括数字,比如100和293.51;或者文本字符串,比如"hello world”;或者日期时间;还有更加详细的对象,此如某人的全名,其中包含姓改、名字和头衔;再比如货币、颜色、电话号码和邮寄地址等。当然还有更加复杂的值对象。这种对象无状态,本身不产生行为,不存在生命周期演进。 值对象的目的和使用 实体对象相对容易理解,我们常见的类的都可以看成是实体对象。值对象在DDD中相对而言是难以理解并且容易误用的。
为什么需要使用值对象,书中给了一个解释:
使用不变的值对象使得我们做更少的职责假设
个人理解这个还是基于BC的封闭性而言的,使用值对象在不同的BC中进行数据交换,可以避免不同BC对实体对象的状态变更而引发的数据依赖关系,实现最小化的集成。另外可以从目前流行的Stateless Service角度考虑值对象的价值。
开发者因为习惯趋向于将关注点放在数据而不是领域上。在软件开发中,数据库依然占据着主导地位。我们首先考虑的是数据的属性(对应数据库的列)和关联关系(外键关联),而不是富有行为的领域概念。这样做的结果是将数据模型直接反映在对象模型上,导致产生贫血型的领域模型的实体。虽然在实体模型中加入getter和setter并不是什么大错,但这却不是DDD的做法。
值类型用于度量和描述事物,DDD 中建议应尽量使用值对象来建模而不是实体对象,因为值对象非常容易地对值对象进行创建、测试、使用、优化和维护。
关于值对象,它拥有以下一些特征:
- 它度量或者描述了领城中的一件东西。
- 它可以作为不变量。
- 它将不同的相关的属性组合成一个概念整体(Conceptual Whole)
- 当度量和描述改变时,可以用另一个值对象予以替换。
- 它可以和其他值对象进行相等性比较。
- 它不会对协作对象造成副作用
一个对象的方法可以设计成一个无副作用函数(Side-Effect-Free Function) 。这里的函数表示对某个对象的操作,它只用于产生输出, 而不会修改对象的状态。由于在函数执行的过程中没有状态改变,这样的函数操作也称为无副作用函数。对于不变的值对象而言,所有的方法都必须是无副作用函数,因为它们不能破坏值对象的不变性。
最小化集成 在所有的DDD项目中,通常存在多个限界上下文,这意味着我们需要找到合适的方法对这些上下文进行集成。当模型概念从上游上下文流入下游上下文中时, 尽量使用值对象来表示这些概念。这样的好处是可以达到最小化集成,即可以最小化下游模型中用于管理职责的属性数目。使用不变的值对象使得我们做更少的职责假设。
https://www.cnblogs.com/ttaylor/p/15927009.html
DDD 领域驱动设计-三个问题思考实体和值对象 消息场景:用户 A 发送一个消息给用户 B,用户 B 回复一个消息给用户 A。。。
现有设计:消息设计为实体并为聚合根,发件人、收件人设计为值对象。
三个问题:
实体最重要的特性是什么? Message 实体是怎么得来的? 发件人、收件人为什么不是实体?
- 实体最重要的特性是什么? 《领域驱动设计》5.2 实体:
摘录一段:许多对象不是由它们的属性来定义,而是通过一系列的连续性(continuity)和标识(identity)来从根本上定义的。
归纳:
标识(identity) 连续性(continuity) 标识在实体中的另一种体现就是唯一和不可变,其概念在很多资料中有说明,这也是实体最重要的特性。
我有一个双胞胎哥哥,我们俩出生的时候,长得一模一样,以至于我们的爸妈都分不清,不得已他们在我们脖子上系个项链来标记:谁是老大?谁是老二?其实这个“标记”就可以看作是实体的标识,只不过是用项链来标识的,就像我们在项目中使用 GUID 方式一样,目的就是用来体现标识,但不管用什么方式表示,这个标识必须在这个特定环境下唯一,也就是说,我和我双胞胎哥哥的项链不能完全一样,要不然我爸妈就不能区分我们俩了。
我和我那双胞胎哥哥就这样一天一天的长大,但出奇的是,我们哥俩越长越像,以至于我们互相看对方,都以为自己在“照镜子”一样,但唯一不变的是我们俩脖子上的项链,这也是区分我们哥俩的唯一方式。刚出生的我和现在的我,脖子上的项链是一样的,这也就是实体标识的不可变性,也就是说刚出生的我和现在的我是同一个人,项链只不过在我成长的过程中起到“标记”的作用(当然也可以是手带、脚环之类的信物),它会“陪伴”我的一生,这个“陪伴”的过程,可以理解为实体的另一种特性-连续性。
有一天,我们镇要统计双胞胎的分布情况,然后调查人员来到我们家,问我们爸妈:“你们家里有没有双胞胎?几对双胞胎?龙凤胎?还是。。。”,然后我爸妈就报上:“一对双胞胎-两个小子”,然后调查人员就做了笔记走了。在这个过程中,他们丝毫没有提及我脖子上的“项链”,虽然它在我爸妈眼里是那么重要(用来标记我们哥俩),但在调查人员眼里却什么都不是,他们只需要知道我和我双胞胎哥哥是什么样的双胞胎就行了,这也就是实体和值对象的根本区别:实体不仅需要知道它是什么?而且还需要知道它是哪个?而值对象只需要知道它是什么?
特定环境下,实体和值对象的区分例子有很多,比如《领域驱动设计》书中所说的“体育场座位例子”和“ Custorm-Address 例子”等等,但大部分都是强调实体的标识特性,却很少提及连续性,那什么是连续性?这部分内容,在《领域驱动设计》中5.2实体章节中最后部分有提及,但都是零碎的概念性文字,如果不注意的话,很容易会被忽略掉。
摘录一段:只要一个对象在生命周期中能够保持连续性,并且独立于它的属性(即使这些属性对系统用户非常重要),那它就是一个实体。
这个内容可以结合上面我和我双胞胎哥哥的例子进行理解,“项链”会陪伴的我一生,这段话可以拆分对应理解:项链-标识、一生-生命周期、陪伴-连续性。也就是说连续性不能理解为生命周期,它应该理解为:标识在实体生命周期内体现出连续性。
- Message 实体是怎么得来的? 结合上面实体特性的理解,Message 实体是怎么得来的,就很好理解了,消息场景毫无疑问聚合的是消息,消息实体是怎么得来的?可以换个角度理解:为什么把消息设计为实体?首先看下消息实体符不符合实体的两个特性。
标识(identity):消息场景中消息的区分通过什么?标题?内容?这些都不行,为了保证消息的唯一性,必须使用标识进行区分,而且必须不可变。消息场景中,有可能会出现标题和内容一样的消息,但这却不是同一个消息,就像我和我那双胞胎哥哥,长的一样,却不是同一人,可以这样说:标识的作用就是为了区分,而消息也必须要区分,所以。。。
连续性(continuity):一次我们家吃饭的时候,我一不小心把饭碗给打碎了,然后我妈就痛打了我一顿,她有个做笔记的习惯,记录我们哥俩的日常生活,比如这次需要记录一下:今天打了谁?但当时她打完我之后,却不记得是打了我?还是我哥?然后她就挨个看我们的屁股和项链,来确定今天打了谁?这就是标识在生命周期中连续性的部分体现。消息场景中,在某一阶段需要对消息进行处理,这个处理需要通过标识来明确处理的是哪条消息?这个对消息处理过程的体现就是连续性,有时候连续性需要在标识明确的情况下,但还有一种是其自身的生命周期连续性,比如从消息的创建,到管理,再到最后的销毁,这个过程就是消息实体的连续。
上面的分析说明消息实体符合实体的两个特性,也就是说消息可以设计为实体,至于怎么得来的?可以这样理解,消息场景首先考虑的是消息,就像我们家的双胞胎,首先考虑的是我和我那双胞胎哥哥。
- 发件人、收件人为什么不是实体? 在之前的一篇博文中,园友鼻涕成诗有这样的疑问:联系人作为值对象这一点有点不太理解,好处是什么?我当时是这样回复的:
联系人作为值对象,因为他不在消息系统中存储,是从外部获取的,而且它的存在要依附于消息,在消息系统这个业务场景中,如果脱离了消息,它就没有什么意义,对于消息而言,我只要知道这个联系人的内容是什么就行了,而不需要它具体什么哪个,人?还是邮箱?这个它并不关心,不是说把联系人作为值对象有什么好处,而是在这个业务场景下,这样设计比较合理些。
回复内容现在看来有些牵强,先不讨论对与错,按照上面消息实体的分析模式,在消息场景下,看下发件人、收件人(可以统称为联系人,发件人和收件人有可能为同一联系人)是否具有实体的一些特性。
标识(identity):联系人是否具有标识?也就是说联系人需不需要进行区分?答案当然是要进行区分,要不然收件箱、发件箱就没办法针对收件人、发件人进行标识,而且联系人有可能名称相同,但是两个不同的联系人,也就是说在消息的整个应用场景中,联系人是必须要唯一标识的,不管它扮演的角色是发件人,还是收件人,这个“角色扮演”概念只是针对某一具体消息来说,联系人所存在的意义(在这个消息中,这个联系人是发件人,但在另外一个消息中,有可能是收件人),但相对于整个消息场景,这个联系人标识是唯一的,而且是不可变的。
连续性(continuity):这个可能没有消息实体的连续性好理解,联系人的连续性其实是依附于消息实体而言,它如果独立出来,自身在消息场景中,是没有连续性概念的,就比如在创建消息的时候,我需要判断收件人是否存在,存在的话就创建收件人对象,并赋予创建消息的收件人属性,还有就是消息在被阅读的时候,需要判断阅读人是否有阅读权限等等,这一些操作,就体现出联系人的连续性依附于消息实体,但不可否认,联系人的创建、使用、舍弃等操作,都可以理解围绕某一具体消息的生命周期,也就是联系人的连续性,而且在这个过程中,联系人的标识都需要首先被明确。
在之前的理解中,联系人设计为值对象的想法是,把联系人看作是一个值,一个依附于消息实体的具体值,我只需要知道这个值就行了,具体体现就是 SenderID 或 RecipientID,其实这个就是联系人的标识,只是当时被两点所迷惑:
联系人外部存储:在消息场景中,联系人的获取是从外部获得的,也就是说联系人不在消息场景中存储,也不进行管理,只是一个获取操作,这个和一般的实体场景不太一样,但仔细一想,不管它是从哪里获取的,这个不应该在消息场景中所关心,我应该专注于联系人在消息场景中的连续性。 联系人依附于消息:这个是最重要的迷惑点,或者说是我根本不了解实体和值对象到底应该是什么?联系人独立于消息,在消息场景中,没有任何意义,但不能因为这一点,就把它设计为值对象,有很多实体是依附关系,只要它存在标识和连续性,那它就是实体。 把联系人设计为值对象当然也有“好处”,比如可以减少对联系人的管理,因为如果联系人设计为值对象,那它就是一个值,也就没有对象的概念,但出来混的迟早是要还的,我要加一个用户禁言功能,这个在现有的设计中就不好进行实现。像这种依附性实体的场景也很多,比如购物车应用中的 Order 和 Custorm,Custorm 依附于 Order,这个首先需要明确的是购物车应用场景,如果是其他的场景下,那 Custorm 就不存在依附关系。
我和我双胞胎哥哥出生的时候,在我们的保温箱上,除了需要标明我们两个的”身份“之外,还需要标明我们爸妈的”身份“,具体标识可以用身份证号,这个就像消息实体中的 SenderID、RecipientID 一样,虽然它是一个”值“,但我还需要知道它具体标识的是哪个对象,因为我不仅需要它表示的值是多少,我还需要知道它所代表的对象是哪个,就比如我和我双胞胎哥哥要根据这个身份证号,找到我们的父母一样。
- 发件人、收件人是值对象?还是实体? 话不言多,总之一句话:发件人、收件人(联系人)需要设计为实体。
消息场景实体和值对象:
Message 消息实体和 Contact 联系人实体。 值对象若干(如 MessageState、MessageType 等)。 概念理解:
What’s the Single Responsibility of an Entity in Domain Driven Design? 领域模型-谈实体对象和值对象 DDD领域驱动设计基本理论知识总结 领域驱动设计实现之路
https://www.cnblogs.com/xishuai/p/ddd-entity-value-object.html
值对象
值对象也是领域模型中的领域对象。
应该尽量使用值对象建模而非实体对象。即便一个领域概念必须建模成实体,在设计时也应更偏向于将其作为值对象容器,而非子实体容器。因为可以非常容易对值对象进行创建、测试、使用、优化和维护。
阅读本文注意思考:应该建模成实体or值对象?如何实现值对象?
1 为什么使用值对象? 曾经,SaaSOvation公司团队滥用实体建模。在用户和权限等概念进入协作领域前,实体建模并没有给他们带来什么坏处。在项目启动时,他们釆用了常用的建模方式,即将领域模型中所有的属性都映射到对应的数据库表。并且为所有属性创建setter/getter。由于每个对象都有一个数据库主键,各个实体被组织在了一个庞大且复杂的对象网。这种建模方式是一种数据建模方式,很大程度受关系型DB影响,认为所有都需范式化,并通过外键关联引用。后来SaaSOvation团队才知道,全然面向实体的思维方法不仅没必要,而且还浪费开发时间。
在将领域概念建模成值对象时,应将通用语言考虑在内,这是建模值对象的首要原则。
如何确定一个领域概念应该建模成一个值对象呢?注意值对象的特征。
2 值对象的特征 当你决定一个领域概念是否是一个值对象时,需考虑它是否拥有以下特征:
度量或者描述了领域中的一件东西 可以作为不变量 将不同的相关的属性组合成一个概念整体(Conceptual Whole) 当度量和描述改变时,可以用另一个值对象予以替换 可以和其他值对象进行相等性比较 不会对协作对象造成副作用
当你只关心某个对象的属性时,该对象便可作为一个值对象。为其添加有意义的属性,并赋予它相应的行为。需要将值对象看成不变对象,不要给它任何身份标识, 还应尽量避免像实体对象一样的复杂性。
在使用这种方法分析模型时,会发现很多领域概念都可设计成值对象,而非实体对象。
在设计得当时,我们可创建和传递值对象实例,甚至在用完后直接扔了。不用担心客户端对值对象的修改。一个值对象的生命周期可长可短,就像个无害的过客在系统中来往。 从该角度来看待值对象是个很大转变,就像从没有GC的语言转变到有GC语言。
虽然创建一个值对象类型非常简单,但是有时甚至连有经验的DDD开发者都面临难题。 《实现领域驱动设计》对值对象的定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。DDD中描述领域的特定方面,并且是一个没有标识符的对象。
值对象本质上就是一个集。该集合有若干用于描述目的、具有整体概念和不可修改的属性。该集合存在的意义是在领域建模的过程中,值对象可保证属性归类的清晰和概念的完整性,避免属性零碎。
案例
实体人员,原包括:姓名、年龄、性别及所在省、市、县和街道等属性。这样显示地址相关属性就很零碎。 就可将“省、市、县和街道等属性”拿出来构成一个“地址属性集合”,该集合就是值对象。
4 不同状态的值对象 4.1 业务形态 值对象是DDD领域模型中的一个基础对象,跟实体一样源于事件风暴所构建的领域模型,都包含若干属性,与实体一起构成聚合。
实体是看得到、摸得着的实实在在的业务对象,实体具有业务属性、业务行为和业务逻辑。 值对象只是若干个属性集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上独立,但在逻辑上仍是实体属性的一部分,用于描述实体的特征。 值对象中也有部分共享的标准类型的值对象,它们有自己的限界上下文及持久化对象,可建立共享的数据类微服务,比如数据字典。
4.2 代码形态 代码中有两种形态。如果值对象是
单一属性,直接定义为实体类的属性 属性集合,设计为Class类,Class将具有整体概念的多个属性归集到属性集合,这样的值对象没有ID,会被实体整体引用 Person实体有若干单一属性的值对象,比如id、name 也包含多个属性的值对象,比如address
4.3 运行形态 实体实例化后的DO对象的业务属性/行为都非常丰富,但值对象实例化的对象相对简单。除了值对象数据初始化和整体替换的行为外,很少有其它业务行为。
举个例子,一人员实体可有多通讯地址,多地址序列化后可嵌入人员的地址属性。值对象创建后不允许修改,只能用另外一个值对象来整体替换。 若将值对象嵌入到实体,即有如下方式:
4.3.1 属性嵌入 当引用单一属性的值对象或只有一条记录的多属性值对象的实体时
属性嵌入形成的人员实体对象,地址值对象直接以属性值嵌入人员实体
4.3.2 序列化大对象 当引用一条或多条记录的多属性值对象的实体时
以序列化大对象方式形成的人员实体对象,地址值对象被序列化成大对象JSON串后,嵌入人员实体
5 值对象简化DB的最佳实践 传统数据建模大多根据数据库范式设计,每个数据库表对应一个实体,每个实体的属性值用单独列存储,一个实体主表会对应N个实体从表。 而值对象简化了DB的持久化设计,多采用反范式,值对象的属性值和实体对象的属性值保存在同一DB实体表。
还是人员和地址案例,要设计实体和数据模型,通常有如下解决方案:
把地址值对象的所有属性放入人员实体表,创建人员实体、人员数据表 会破坏地址的业务含义和概念完整性 创建人员和地址两个实体,同时创建人员和地址两张表 增加了不必要的实体和表,需要处理多个实体和表的关系,导致数据库复杂性剧增 那有没有设计可使得业务含义清晰,又不会让数据库变复杂?综合这俩方案优势,扬长避短即可:
领域建模时,把地址作为值对象,人员作为实体,这就可保留地址的业务含义和概念完整性 数据建模时,将地址的属性值嵌入人员实体数据库表,只创建人员数据库表。这既可兼顾业务含义和表达,又不会复杂化DB 值对象就是通过该方式,简化DB设计:
领域建模时,将部分对象设计为值对象,保留对象的业务含义,同时又减少了实体数量 数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化DB设计 也有DDD专家认为,要发挥对象的威力,就需优先领域建模,弱化DB作用,只把DB作为一个保存数据的仓库即可。即使违反DB设计原则,也不必大惊小怪,只要业务能顺利运行,无伤大雅。
6 值对象的优劣分析 虽然优势是可简化DB设计,提升DB性能。但若使用不当,其优势很快会成劣势。必须理解值对象的适用场景。
值对象采用序列化大对象的方式简化了DB设计,减少了实体表的数量,可简单、清晰表达业务概念。该方式虽然降低DB设计复杂度,但却无法满足基于值对象的快速查询,会导致搜索值对象的属性值变难。
值对象采用属性嵌入的方式提升了DB性能,但若实体引用的值对象过多,则会导致实体堆积一堆缺乏概念完整性的属性,这样值对象就会失去业务含义,操作也不方便。
所以对照优劣势并结合实际业务场景,才能发挥值对象的最大作用。
7 实体 V.S 值对象 实体和值对象都是微服务底层最基础的对象,一起实现实体最基本的核心领域逻辑。
实体和值对象的目的都是抽象聚合若干属性以简化设计和沟通,有了这一层抽象,我们在使用人员实体时,不会产生歧义,在引用地址值对象时,不用列举其全部属性,在同一个限界上下文中,大幅降低误解、缩小偏差,主要区别如下:
二者都经过属性聚合形成,实体有唯一性,值对象没有。在本文案例的限界上下文中,人员有唯一性,一旦某个人员被系统纳入管理,它就被赋予了在事件、流程和操作中被唯一识别的能力,而值对象没有也不必具备唯一性 实体着重唯一性和延续性,不在意属性的变化,属性全变了,它还是原来那个它;值对象着重描述性,对属性的变化很敏感,属性变了,它就不是那个它了 战略上的思考框架稳定不变,战术上的模型设计却灵活多变,实体和值对象也有可能随着系统业务关注点的不同而更换位置。比如,如果换一个特殊的限界上下文,这个上下文更关注地址,而不那么关注与这个地址产生联系的人员,那么就应该把地址设计成实体,而把人员设计成值对象。 二者在某些场景可互换,因为很难判断到底将领域对象设计成实体or值对象。值对象在某些场景下有高价值,但并非适合所有场景。需根据团队设计开发习惯及优劣分析,才能选择最适合的方案。 比如多人的单位地址是一样的,怎么处理?一方面,许多人可能属于同一个地址,另一方面,许多地址也可能属于同一个人,人和地址既可以分别作为实体而把对方作为值对象,也可以共同作为实体来描述业务,这正是业务设计存在的价值,也是我们赖以生存的生态位,如果业务设计可以非黑即白一板一眼,反倒不需要什么业务架构师了
DDD提倡从领域模型设计出发,而非先设计数据模型。 传统数据模型设计通常一个表对应一个实体,一个主表关联多个从表,当实体表太多,就很容易陷入复杂DB设计,领域模型就很容易被数据模型绑架。所以值对象和实体是相辅相成。
还是那个案例,在领域模型中人员是实体,地址是值对象,地址值对象被人员实体引用。 设计数据模型时
地址值对象可作为一个属性集整体嵌入人员实体 也可以序列化大对象的形式加入人员的地址属性 该案例也可看出,同样一个对象在不同场景,可能设计不同:
有些场景,地址会被某一实体引用,只描述实体,并且其值只能整体替换,这时就可将地址设计为值对象,比如收货地址 某些场景,地址会被经常修改,地址作为一个独立对象存在,这时应设计为实体,比如行政区划中的地址信息维护 参考
实体和值对象:从领域模型的基础单元看系统设计 《实现领域驱动设计》
Author -
LastMod 0001-01-01