DDD系列:什么是“值对象是不可变的”

2023-03-07 13:58:43 浏览数 (2)

"人不能两次踏进同一条河流。"

软件系统是物理世界的映射。在一个没有出现任何变化的物理世界中,是没有必要开发一个软件系统来提高效率的。那么值对象的“不可变”具体是指什么不可变呢?值对象不可变,为什么实体就可变了呢?不可变,是指软件系统中能够唯一确定一个主体的属性不可变。从这个维度来看实体和值对象都不可变

不可变,不是说软件系统中这些关键属性一旦持久化,就不能修改了。是可以修改的。只是能够唯一确定一个主体的属性中,任何一个被修改了,在软件系统中就代表了另一个主体。老的主体在系统中代表的存在就连接不到了,即找不到了。

上面的概念有些抽象,结合例子来解释一下。走两步:

场景一:操作日志

软件系统中的操作日志的格式一般是“什么时间什么人进行了什么操作”。

什么人,在软件系统中如何表示呢?操作人ID。不可变,是指操作人ID相同。如何修改了这个ID,则代表是另一个操作人进行的操作,这个操作与之前的操作人不管这个操作主体的性格、年龄、图像、Title、薪水等属性发生什么变化,只要操作ID相同,就可以认定是同一个操作主体进行的操作。

这个场景,能够唯一确定操作主体的属性就是操作人ID。

场景二:人员的地址

人员的地址代表的是物理世界的一个位置,由省、市、县、街道四个属性组成。

这个场景中,地址是省、市、县、街道四个属性共同决定的。只要这四个属性相同,就代表了物理世界上相同的位置。

值对象的领域模型与数据模型示例

不可变,是指一个人员的省、市、县、街道四个属性任何一个改变了,就变成一个新地址了。即这个人员在软件系统中的地址就映射到了物理世界中另一个位置了。这个人员的老地址在软件系统中找不到了。不可变,指代表的业务含义不可变。联合主键

值对象的代码形态示例

DDD的相关概念补充:在事件风暴中,我们会根据一些业务操作和行为找出实体(Entity)值对象(Value Object),进而将业务关联紧密的实体和值对象进行组合,构成聚合,再根据业务语义将多个聚合划定到同一个限界上下文(Bounded Context)中,并在限界上下文内完成领域建模。 实体(Entity):在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。值对象(Value Object):通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。

区分实体和值对象的好处:在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。聚合根在数据中相当于主表的概念,实体是一般的表,而值对象可以设计成一般表,但是大多数情况下可以依托引用的实体表设计成嵌入属性集或者以Json串的形式存储。实体就是我们一般理解上的业务对象,我们关注他们的生命周期,所以会有全局ID,通过ID来管理追踪它的生命周期。而值对象主要是用于描述的属性集,我们不关注他们的生命周期,更关注它的属性值。同样的五块钱。在超市购物的时候:我有100块钱,你也有100块钱。这里会关心我的钱和你的钱是同一张,同一个编码,同一个组合方式(一张100块,五张20块)吗?显然不会。因为它们的价值是一样的,就买东西来说,所以是不需要ID的。切换到一个货币生产的环境下。会考虑这同样的一张100块钱是否重号或号码错误,显然重号或号码错误的货币是不允许发行的。所以每一张货币必须有一个唯一的标识作为判断,用来追踪它的流通轨迹、生命周期及判定是否是有效的货币。可见,值对象是基于上下文的 当前上下文的值对象,可能是另一个上下文的实体 为什么会这样设计?主要还是为了实现聚合的解耦。在管理这个实体的聚合中,我们需要通过ID来管理这个实体的生命周期,而当这个实体数据流转到其它聚合时,这个实体的数据值就不允许修改了。这样可以保证一份数据只在一个地方修改,而可以在多个不同的业务领域使用,保证业务的“高内聚和低耦合”。当前聚合中的值对象数据可能来源于其他聚合,它们以数据冗余的方式完成不同领域中数据的流转和共享。在当前聚合中的值对象以实体或聚合根的形式在另外一个聚合中存在,完成数据的集中维护和管理。而在当前的聚合中它则以值对象的形式存在,被聚合内的某一个实体引用。例如:在订单聚合中,订单实体有收货地址这个值对象。在生成订单实体时,会从个人中心的客户聚合中,获取地址实体数据组合成订单聚合的地址值对象。订单实体可以整体引用和修改地址值对象的数据,但不允许单独修改地址值对象的某一个属性数据。所有地址数据的新增和修改等维护,都只能在客户聚合中完成,这样就可以实现业务职责的高内聚,也就是说“如果你要修改某个业务行为,只需要修改一处就可以了。”由于不同聚合中实体和值对象的这种关系,值对象还有一个重要的使用场景,那就是记录和生成业务的数据快照。值对象以数据冗余的方式记录业务发生那一刻前后序聚合之间的业务数据,还原业务发生那一时刻的数据场景。比如订单聚合在下单时会记录订单生成那一刻的商品和收货地址等概要基础数据信息,我们称之为跟单数据。这时订单聚合的商品和收货地址是以包含多个属性的属性集以值对象的形式存在的,它们被订单聚合根引用。属性集值对象的设计方式与通过商品ID或地址ID单一属性值对象关联的方式不同,当商品或地址的源端聚合的商品实体或地址实体数据变更后,不会影响订单聚合中商品和收货地址值对象的快照数据,这样就可以记录业务发生那一刻的业务快照数据了。即使源端商品或地址所在聚合出现服务不可用的情况,也不会影响订单聚合中商品或地址相关的业务逻辑,很好地实现了应用的解耦和故障隔离。

0 人点赞