写点什么

原创 | 使用 JPA 实现 DDD 持久化 - O: 对象的世界 (3/3)

发布于: 2020 年 08 月 11 日
原创 | 使用JPA实现DDD持久化- O:对象的世界(3/3)







领域模型

对象模型中有一部分与技术完全无关,纯粹用于描述问题域中的业务实体和业务逻辑的类,它们组成了领域模型(domain model)。领域模型主要由实体(Entity,又称为引用对象)值对象(Value Object)组成。

1、实体(Entity,又名引用对象Reference Object)

领域模型中,最关键的一组类是实体类。实体类用于建模问题域中的关键概念,例如顾客(Customer)、订单(Order)、账户(Account)等。实体拥有独立的生命周期,通常是有状态的,在实体的生命周期中,其属性值可以发生多次改变。

实体通过标识符(ID)来进行识别,属性值的变化无关紧要。类型和标识符相同的两个实体视为同一个实体,即使其余的属性值完全不同。标识符不同的两个实体视为不同的实体,即使其余的属性值都相同。实体的重点是通过ID表明它“是谁”,而不是通过属性表明它“是什么样子”

为了思考什么是实体,思考下面的问题:

  • 五岁的我和现在的我是同一个人吗?

  • 一个人杀了人之后逃走了,为了逃避罪责,他染了发,整了容,改姓埋名,甚至做了变形手术。后来他被抓到了,他还是不是以前犯罪的那个人,需不需要承担杀人的后果?

上面问题的答案都是肯定的:是同一个人。人是一种实体类型。年龄、发色、相貌、姓名、甚至性别的变化都不影响他是同一个人。可以认为每一个人都有一个隐含的标识符,唯一标识了他的身份,把一个人和另一个人区分开。注意:姓名和身份证号码都不是必然的标识符,因为这些属性都是可以更改的。

实体的关键特征是:

  • 拥有一个唯一且不变的标识符。通过标识符来识别相同的实体对象和区分不同的实体对象。

  • 拥有独立的生命周期。

  • 在实体生命周期中其状态(属性值)可以发生变化。

2、值对象(Value Object)

领域模型中还存在一类对象,它们用于描述领域实体的某个方面,而本身没有概念标识,我们关注的是它“是什么样”而不关心它“是谁”,我们把这种类型的对象成为值对象。它们本质上是“披着对象外衣的值”

值对象的例子是电子邮箱Email、金额Money和订单项目OrderLine等。值对象可以包含单个属性,例如Email类包括一个属性address,也可以包括两个或多个属性,例如Money类包括amountcurrency两个属性,OrderLine类包括quantity、priceproduct属性。值对象的属性类型可以是简单值,也可以是其他值对象,甚至是实体。例如值对象OrderLine类的price属性类型是值对象Money,而product属性类型是实体类Product

值对象用于描述它所从属的实体的某个方面的属性。如同我们可以用简单值182来描述某个人(实体)的身高(以厘米表示)一样,我们可以5美元(即amount = 5, curreny = USDMoney值对象)来表示某个订单(实体)的金额。

在领域模型中,独立存在的简单值和值对象都没有意义。单独存在的数字“182”没有意义,除非给赋值给人这个实体对象的身高属性,表明“这个人的身高是182”;单独存在的Money类型值对象“5美元”也没有意义,除非被赋值给订单实体的金额属性,表示“这个订单的金额是5美元”。

值对象和简单值(数字、字符串、枚举等)具有同等的地位,低于实体的地位。和简单值一样,值对象是实体的内部状态的一部分,位于实体的边界之内,不具有独立的生命周期。

值对象没有概念标识,其意义完全体现在它的属性上。所有的“5美元”都是相同的,区分“这个5美元”和“那个5美元”没有意义。相同类型的两个值对象,如果它们的属性值完全相同,就可以认为是等同的,可以相互替换;只要有一个属性值不同,就认为是不同的两个值对象。我们愿意交换相同面值的两张美元(值对象)因为它们是等价的;但绝对不愿意交换相同体重的两个婴儿(实体),因为每个人都只想要自己的孩子。

值对象在本质上是不可变的。改变了属性值的值对象实质上不再是原来的值对象,而是另一个值对象。因此对于实体的值对象属性来说,没有修改,只有替换。如果订单的金额从5美元改成了7美元,就是将原来的5美元扔掉,换上一个7美元,而不是将原来的那个5美元的amount属性值改成7。应该这样编码:

order.setPrice(new Money(7, Currency.USD));

而不是这样编码:

order.getPrice().setAmount(7);

实际上,Money类根本不应该提供setAmount()setCurrency()等方法。Money类应该是只读的,通过构造函数或静态工厂方法在对象创建时对属性赋值,不提供用于修改属性值的setter方法,从而保证Money类是不可变的。

值对象的关键特征是:

  • 没有唯一标识符。其意义完全体现在其属性上。

  • 依附于实体对象而存在,是实体的内部状态的组成部分,没有独立的生命周期。

  • 其状态(属性值)不可变。

3、建模实体和值对象

有一个简单的测试可以用于区分实体和值对象。如果在领域语义中的一个领域对象可以用一个简单的词汇直接提及,例如“员工张三”,“订单31456”,那么这些对象通常可以建模为实体。如果总是需要将某个领域对象称为“某某的某某”,例如“员工张三的住址”,“订单31456的金额”,这样的称呼强烈暗示这些对象是用来描述另一些对象的某些方面的属性的,那么这些对象通常应该建模为值对象。

即使是像金额、Email等等可以用一个简单的数字或字符串表示的属性,也应该建模为一个值对象。将它们建模为值对象一方面丰富了领域模型的内涵,另一方面可以对数据进行校验,防止引入非法的值,还可以在值对象上面定义各种方法。例如可以在Email类的构造函数或工厂方法中校验email字符串,当这个字符串不符合email规范时抛出异常。这样就可以保证每一个email都是格式正常的。

如果对于将某个领域概念建模为实体还是值对象暂时没有把握,那么首先尝试将领域对象建模为值对象。随着分析设计的进一步深入,拥有足够信息之后,再来决定是否将值对象升格为实体。将所有领域对象一股脑建模为实体是很多人常犯的错误。

如果错误地将值对象建模为实体,就必须跟踪它的生命周期,这会大大增加编程的复杂度,而且容易违背业务领域的语义。例如,值对象(不管是单值的还是多值的)是它所在的实体的一部分,其生命周期由实体的生命周期来决定。这意味着当我们删除实体时,它拥有的值对象(不管是单值的货多值的)应该会自动一并删除,不需要人工编程实现(JPA保证了这一点)。一个例子是订单删除时所有的订单项目会一并删除。如果将订单项目建模为实体,就做不到这一点。我们必须手工删除所有的订单条目,而且必须在删除订单之前,以免违背引用完整性。另一个例子是,如果将订单条目建模为值对象,当订单条目有增删时,我们只需要在订单对象上用新的订单条目集合替换掉原来的订单条目集合,不需要区分哪些条目是新增的,哪些是保留的,还有哪些删除了(JPA也会自动实现这一点);但如果将订单条目建模为实体,就必须手工编码来处理这个问题了:将新集合和旧集合比较,找出哪些是新增的条目,将它们添加到仓储中,找出哪些是要删除的条目,从仓储中将它们删除。这些人工编码不但繁琐而易于出错,而且不符合业务领域语义,在业务和技术之间出现裂缝。

更加严重的问题是所谓“别名问题”。举个例子,我们定义一个实体类Address(地址),包含了省、市、区县、街道和门牌号等可变属性。同时定义了实体类Employee(员工),它包含一个类型为AddresshomeAddress属性。如果员工A和B曾经住在一起,我们创建了一个地址C,代表他们的当前居住地址,并将它同时赋值给两个员工的homeAddress属性。如果将来有一天,员工A搬到了别的地方,修改了他的居住地址中的某些属性,就会导致员工B的居住地址也改了,变成和A的地址一模一样!原因就是因为两者指向(引用)同一个实体,而不是各自拥有独立的地址!因此,不应该将Address建模为实体,而应该变魔为不可变的值对象。这样,员工A和B就各自拥有自己的居住地址,而不是引用一个外部地址实体。

4、值和关联

在企业应用中,真正需要持久化的就是领域模型中的实体对象的状态。

实体的状态包括它的全部属性。实体中的属性(Property)可以分为两大类:

  • 值属性(Attribute):属性类型是值(包括简单值如数值、字符串、布尔值、枚举、日期等,或值对象如EmailMoneyAddressOrderLine等)或值容器(元素内容是值的ListSetMap或数组等)。

  • 关联属性(Association):属性类型是实体或实体容器(元素内容是实体对象的ListSetMap或数组等)。

值是实体对象的内部组成部分,而关联指向的是外部的另一个或一批实体。值位于实体的边界之内,关联位于实体的边界之外。实体与值对象之间的关系是组合(Composition)关系,而实体与实体之间的关系是关联、聚合或继承关系。

在UML的类图中,值对象可以以类的形式出现:

@startuml
class Customer <<Entity>>{
- name
- phoneNumber
}
class CompanyCustomer {
}
class PersonalCustomer {
}
class Address <<ValueObject>> {
- province
- city
- street
- postalCode
}
class Order <<Entity>> {
- number
}
class Money<<ValueObject>> {
- amount
- currency
}
class OrderLine <<ValueObject>> {
- quantity
+ price()
}
class Product <<Entity>> {
}
Customer *--> "*" Address : shippingAddresses
Customer <|-- CompanyCustomer
Customer <|-- PersonalCustomer
Order *--> Address : shippingAddress
Order *--> "*" OrderLine : lineItems
Order --> Customer
Order --> Money : totalPrice
OrderLine --> Money : unitPrice
OrderLine --> Product
@enduml

也可以作为属性类型出现在类的内部,指明类的Attribute的类型:

@startuml
abstract class Customer <<Entity>>{
- name: String
- phoneNumber: String
- shippingAddresses: Address *
}
class CompanyCustomer {
}
class PersonalCustomer {
}
class Address <<ValueObject>> {
- province: String
- city: String
- street: String
- postalCode: String
}
class Order <<Entity>> {
- number: String
- shippingAddress: Address
- totalPrice: Money
}
class Money<<ValueObject>> {
- amount: double
- currency: Currency
}
class OrderLine <<ValueObject>> {
- quantity: int
- unitPrice: Money
+ price(): Money
}
class Product <<Entity>> {
}
Order *--> "*" OrderLine : lineItems
Order --> Customer
OrderLine --> Product
Customer <|-- CompanyCustomer
Customer <|-- PersonalCustomer
@enduml

由于值对象OrderLine结构更加复杂,内部还拥有指向实体Product对象的关联属性,上图中仍然用类的形式呈现它。而MoneyAddress就不再单独作为类出现,而是作为实体中的Attribute的类型,例如OrdertotalPrice属性和OrderLineunitPrice属性的类型为值对象MoneyCustomershippingAddress属性的类型是值对象Address

5、总结

综合上述3节课程,在对象模型中,纯粹代表业务领域概念、与具体软件技术无关的子集称为领域模型,用于建模业务领域的概念、规则和逻辑。一般而言,只有领域模型中的领域对象才需要进行持久化。

领域模型中的领域对象可以分成两类:实体和值对象。实体拥有唯一标识符,是领域模型中最重要的对象。实体有独立的生命周期,在其存活期间,其余的属性可以发生变化,但标识符不变。值对象没有标识符,它的意义全部体现在其属性上。值对象作为实体的内部属性的类型,用于标识实体某些方面的特征。值对象没有独立的生命周期,而是从属于它所属实体的生命周期。

实体和值对象的属性可以分类两类:类型为简单值或值对象的单值或多值的Attribute,和类型为其他实体的单值或多值的Association。本书后面将它们分别称为值属性关联属性。这两者都需要持久化到外部媒体中,以便在系统重启时可以重建整个对象图——包括每个实体的内部状态以及实体之间的关联关系。



详细内容请戳这里↓↓↓

原创 | 使用JPA实现DDD持久化- O:对象的世界(3/3)



这一节就讲到这里,下一节我们讲"R:数据的世界"



如果觉得有收获,点个【赞】鼓励一下呗!



发布于: 2020 年 08 月 11 日阅读数: 55
用户头像

高级架构师,技术顾问,交流公号:编程道与术 2020.04.28 加入

杨宇于2020年创立编程道与术,致力于研究领域分析与建模、测试驱动开发、架构设计、自动化构建和持续集成、敏捷开发方法论、微服务、云计算等顶尖技术领域。 了解更多公众号:编程道与术

评论

发布
暂无评论
原创 | 使用JPA实现DDD持久化- O:对象的世界(3/3)