识别实体和值对象非常重要,正确与否将直接影响聚合的设计。聚合是边界在DDD中,聚合是实体和值对象之间的边界。一个聚合对外表示一个完整的领域概念。遵循面向对象设计的基本原则,聚合往往由多个小的高内聚领域概念组成。聚合内部的领域模型形成了一棵树。树的根必须是一个实体,可以称为聚合根(AggregateRoot)。当然也可以称为根实体(RootEntity),是聚合的唯一入口。或导出。例如,订单聚合定义了订单根实体,它是订单聚合的唯一代言人。在一个限界上下文的所有领域模型(实体和值对象)中,根据关系的强弱和概念的完整性,将其划分为多个聚合体,就像草原部落由蒙古包组成一个松散的聚居社区一般来说。考虑到值对象和实体的区别,如果需要对其生命周期进行管理,值对象不可能脱离聚合的边界而独立存在。这意味着当我们要识别领域模型的聚合时,实体与值对象之间的强弱关系不会影响聚合边界的定义。只要实体和值对象之间存在关系,无论关系强弱,值对象都必须与存在关系的实体放在同一个聚合中。如果一个值对象和多个实体之间存在关系,要么意味着多个实体属于一个聚合;或者说需要将值对象复制成多份,放在不同的聚合中,如下图:,聚合边界的识别变成了对实体关系强弱的判断。只要正确识别实体和值对象,在识别聚合时就可以不再考虑值对象,可以降低识别难度。上下文的影响虽然我们知道实体和值对象的本质区别在于它是否具有唯一标识(identity),但是很多时候,这种区别仍然显得似是而非。更何况,实体和值对象的定义并不是绝对的。在不同的上下文中,相同的领域概念可能被定义为不同的设计类型。例如一张如下图所示的钞票:在购买语境中,买卖双方只关注钞票的面值和币种。只要值相等,就可以认为是同一个对象,所以需要定义为值对象;每一张钞票都有一个唯一的标识,即使是100元人民币,只要标识不同,就会被认为是不同的对象,所以定义为一个实体。因此,要正确识别实体和值对象,需要结合特定的上下文。尽管如此,仍然缺乏相对客观的特征识别标准。为此,我总结了以下几个特点。相等性识别实体和值对象,首先可以从相等性来判断。只要一个领域模型对象的属性值相等,就认为是同一个对象,应该先建模为值对象;否则,需要为领域模型对象定义一个唯一标识符,并将其建模为一个实体。注意:在进行相等性判断时,作为唯一标识的ID不能作为领域模型的属性。比如地址字段的概念,只要它的属性值country,province,city,street,zipcode都相等,就可以认为是同一个地址,Address类应该定义为一个值对象。对于熟悉的order字段概念,显然需要为其分配一个唯一的订单号,因为理论上可能存在两个不同的订单,除了订单号外,其他属性相同,而Order应该定义为一个实体。然而,在判断是否相等时,ID和属性之间可能存在隐含的对应关系。例如,图书作为出版行业的正规出版物,有唯一的ISBN号,相当于图书领域概念的ID,所以Book应该定义为一个实体。在判断Books是否相等时,也可以判断没有ISBN是否相等。基本上只要书名、作者(译者)、出版社、价格、出版日期、版本、页数、字数等属性值相同,也可以认为是同一本书。这是否意味着可以将Book定义为值对象?显然,在做相等判断时,考虑的属性越多,就会有多个属性的组合,形成一个“隐藏”的唯一标识特征,一些反映业务规则的ID,本身就是根据属性值来定义的。例如,可以根据承运公司二字代码、航班号、起降机场三字代码、航班日期确定航班的唯一标识。当然也可以通过唯一标识来判断航班是否相同,也可以根据映射的多个属性值来判断是否相同。这会让人在区分实体和值对象时犹豫不决。比如腾讯会议的会议号就是Meeting的标识。在比较会议的平等性时,如果考虑会议号以外的其他属性,如会议名称、会议类型、开始时间、结束时间、创建者、创建时间等属性是否可以判断会议的平等性?因此,除了判断相等性外,还必须考虑不变性。InvarianceEricEvans建议将值对象定义为不可变类,其实是因为以值判断的值对象应该具有不变性。还是以购买上下文中的钞票为例,50元+50元=100元,这100元是另外一张和原来50元不一样的钞票:相反,除了ID,其他属性值一个对象的of是可以修改的,不需要在创建一个新的对象时,可以认为领域对象是可变的,应该被认为是一个实体。对于上面提到的Meeting对象,只要meetingId值不变,即使会议名称、会议类型、开始时间、结束时间等属性值发生了翻天覆地的变化,我们仍然认为是同一个会议.显然,Meeting应该被定义为一个实体。考虑一个典型的订单聚合:为什么我们将订单聚合中的OrderItem定义为一个实体?如果不考虑ID属性,只要orderId、product、quantity的值相同,就可以认为是同一个订单商品。但是,行项目的数量值是可以更改的,具有更改数量的行项目不会被视为不同的行项目。行项目的可变性决定了它应该被定义为一个实体。为什么要将OrderItem的Product属性定义为一个值对象呢?要知道,Product类型还定义了productId属性。既然有身份,难道不应该定义为实体吗?因为在ordercontext中,商品的productId来自于productcontext在订单聚合中,productId可以看作是Product类的属性值。只要productId、name、price的值相同,就可以认为是同一个产品,它们的值保持不变。这就是Product被定义为值对象的原因。独立性即使在考虑相等性和不变性时,也有一个例外要考虑独立性特征。值对象作为实体的属性,必须依附于实体,不能单独存在;如果域对象同时满足相等性和不变性,则可以定义为值对象;但是,如果它单独存在,并且需要对其生命周期进行管理,则需要将这样的类“升级”为实体。在出勤的背景下考虑休假字段概念。由于中国农历假期,每年都需要配置新的假期。假期概念对应的Holiday类的定义是:显然,这个类的所有属性值都是相等的,所以可以认为是同一个假期。holiday的值一旦被修改,也可以认为是不同的holiday,即Holiday类同时满足相等性和Immutability,应该定义为值对象。但是在考勤上下文的领域模型中,Holiday类是完全独立的,不依赖于任何其他实体,也需要管理生命周期。这时,它应该“升级”为具有独立性特征的实体。以上三个特征的优先级不分先后,需要综合考虑。如果还不能判断,遵循优先原则:优先将领域概念建模为值对象。
