看DDD的书时总感觉里面有许多晦涩难懂的术语,这其中就包括实体与值对象。最近的一个项目期间就有客户同学不断问起这两者的定义和区别,这个项目是客户邀请我们用DDD工作坊的方式带领客户团队对其运行多年的运营商CRM系统中的促销模块做微服务系统改造规划,我有幸参与其中。
概念
正式工作坊前对客户团队做了一个DDD的导入培训,培训时讲到实体与值对象时,分别介绍了两者在《领域驱动设计》书中原文的定义。
实体(Entity), 主要由标识定义的对象。它可以是任何事物,只要满足两个条件即可,一是它在整个生命周期中具有连续性;二是它的区别并不是由那些对用户非常重要的属性决定的。
值对象(Value Object),用于描述领域的某个方面而本身没有概念的对象称为值对象,值对象被实例化之后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,不关心它是谁。
光看定义,理解起来有点生硬,再呈上一张详细对比图以帮助大家理解。
这张图讲解完后,技术的同学开始若有所悟的点头,可是作为业务代表们似乎更晕了。”什么叫持久化?“, ”什么叫Equals方法?“, "为什么实体可变而值对象不可变?"
还好早有准备,公司DDD培训里的经典案例京西商城可以派上用场了.
”我们以电商为例,通常电商用户自己可以配置一个或多个收货地址,同时可以对用户收货地址进行修改、删除,像这个用户收货地址就是实体;而当用户下订单时从用户收货地址里选取一个地址作为订单送货地址,这个订单的送货地址就应该是个值对象,因为订单送货地址是随着订单创建后就固定下来了,它不会因为用户修改了用户收货地址而导致订单的送货地址发生变化,所以订单送货地址里应该保存的是地址的完整信息而非用户收货地址的Id“
有的业务同学们似乎理解了,还帮着向旁边的小伙伴解释。看来关于实体、值对象的理解似乎统一了认识,不用再解释什么叫持久化和Equals方法了。
二义性
DDD工作坊很重要的一环是事件风暴,通常会按场景来开展,包括三个关键步骤,第一步梳理领域事件, 第二步识别角色和命令, 第三步是识别聚合。刚做完第一个场景就有爱思考的同学提出困惑了:”DDD导入培训时说聚合是一组相关对象的集合,可现在看这个结果全是独立的,看不出哪有集合。“ 这确实是个好问题,因为在这阶段梳理出来的结果在未完成聚合划分、模型细化的前提下,还真说不准它会是一个聚合根实体、普通实体还是值对象。为解除语义上的困扰于是我们把这个第三步改叫做”识别领域对象“, 领域对象包括实体和值对象。(在胡皓老师新年礼物的实战工作坊操作手册里把这步叫识别领域名词)
实现
当项目开始做领域建模时,围绕实体与值对象还发生了一个小插曲。做模型细化时需要把聚合根实体下有哪些实体、值对象识别出来,有开发同学提出疑问:”值对象和实体是不是就是对应到Java代码里的基本数据类型和引用类型?“
不全是,值对象的实现时强调的是不可变。基本数据类型可以满足不可变,引用类型也可以做到。比如Java里String类型就是一个不可变的引用类型。
在Java里具体需要如何定义值对象,需要首先了解Java里判断对象相等的实现方式。1. 判断引用是否相等;2. 判断Id是否相同;3.判断特定属性集是否相等;
所以在Java代码里区分实体与值对象,取决于判断两个同类型的实例是否相等的方式, 实体通过引用或Id比较来判断, 值对象通过属性比较来判断。这也是为什么在前文提到的值对象需要实现equals方法。
案例
开发同学继续好奇的问道:”像实体与值对象这么偏技术的,是不是不需要在DDD工作坊阶段就确定下来,只要标识它是一个领域对象就行了?“
还正在思考怎么组织逻辑来回答这个问题, 作为业务代表的产品经理打断了我们,说要讨论对”用户促销活动“这个聚合进行细化。
这里先简单介绍一下促销中心的业务背景。
促销活动配置管理员会去管理后台配置一系列的促销活动,每个活动里会包含一些参与条件及可享受的折扣优惠;当用户达到参与条件并在柜台签署一份参与活动的协议后,营业员就会把用户参与的促销活动提交给促销中心系统。计费系统会在费用结算时根据用户参与的促销活动进行费用的折扣减免。
用户促销活动,就是指用户参与了的促销活动,代表用户与促销活动的订购关系。
下图是一些典型运营商促销场景。
基于对于业务的理解,大多同学都认为在用户促销活动里应该把配置管理的活动优惠作为值对象把关键属性存下来,这样当配置管理员调整活动的折扣优惠时不会影响到已经办理过了的用户促销活动优惠计算。于是用户促销活动的领域模型会是这样的,如下图:
产品经理看了用户促销活动的领域模型思考片刻后提出促销优惠应该是个实体。为了避免是产品经理对实体与值对象的理解偏差,我把二者的定义、区别以及京西商城用户地址的例子又和他讲了一遍。
他听完后更加坚定的说:"那就是的,促销优惠在用户促销活动下是个实体,我希望配置管理员修改配置活动里的优惠后,用户参与了的促销活动也立即生效。"
”那这样用户话费计算不就是有不确定性了?“ 开发同学着急的问道。
产品经理解释道:”因为正常来说,我们的促销活动一经发布就不会修改了。如果有变化都是通过策划新的活动来替代它,而不会修改原来的活动。但有一种情况会修改,那就是配置人员配置错误了,这个时候就希望领导审批后能够通过修改活动配置来纠正并同步修正用户已经办理了的促销活动。“
看来领域模型中的实体、值对象的识别还是得业务和技术一起对齐业务认识, 光凭技术同学的经验是有风险的。
根据产品经理的描述,于是赶紧调整了”用户促销活动“的领域模型,把活动名称、促销优惠都通过关联活动实体来获取。
最后
DDD提供的是方法论,不是非黑即白的公式,究竟应该是实体还是值对象应该根据具体的业务场景来定。
正准备发文的时候,好学的客户同学发来了新的问题。