实体
1.介绍
在领域中,一个由身份而不是属性值定义的客观概念就是实体,这个身份可以由一个唯一标识确认
2.特点
- 唯一标识且不变
- 业务具有连续性
有ID其实意味着:一个ID串起了许多数据表,意味着以它为主线,组织起了很多的业务数据和历史数据
3.案例理解
业务背景
以一个电商应用中的收货地址信息为例,讲解实体的建模
电商应用中一般都会包含地址管理功能,该功能由用户收获地址服务提供支持。通过地址管理功能,用户可以与预先录入地址信息,并将某个地址设置为默认地址。在用户下单时,可以直接选择已录入的地址信息,避免多次重复输入,提升用户体验
收货地址信息一般包括省,市,区,街道,门牌号,收件人姓名,收件人手机号等信息,这些信息关系非常紧密,而且需要形成一个整体才会有意义。单独的街道,门牌号是没有办法构成收货地址的,因此应该将这些关系非常密切的信息抽取出来形成一个整体概念,也就是建模成领域模型
两大要素
对于地址服务来说,将某个地址设置为默认地址,这句话体现了两个逻辑
- 业务连续性:用户可以录入自己的收货地址,并且在后续可以对其进行某些业务操作,例如,设置位默认地址,取消设置默认地址,这体现了收货地址这个领域模型的连续性,因为后续操作的都是同一条地址记录
- 唯一性:用户可以将某个地址设置为默认,而不是将其他地址设置为默认,说明这些地址信息之间是需要区分的,这体现了唯一性
建模结果
因为上面的两大要素,所以在用户地址服务中,自然而然地将地址信息建模成实体,并且在用户添加地址时赋予一个业务上的唯一表示
注意
对于实体的唯一标识,通常将其建模为值对象
代码
/**
* 地址唯一标识值对象
*/
public class AddressId {
private final String value;
public AddressId() {
this.value = value;
}
public String getValue() {
return value;
}
}
/**
* 地址实体
*/
public class AddressEntity {
/**
* 唯一表示
*/
private AddressId addressId;
/**
* 省
*/
private String province;
/**
* 市
*/
private String city;
// 忽略其他属性
/**
* 修改地址信息
*/
public void changeAddressInfo(String province,String city) {
// 修改地址信息
}
}
4.建模
介绍
我们在建模时,什么样的领域概念是实体?领域专家什么样的话术可能暗示这个概念是实体?
错误方法
最直接的办法是把领域概念都建模为实体,这可能是造物主创建世界的方式,因为世界上没有完全相同的两片叶子。但是,对于你的系统而言,这绝不是一个好办法。我们已反复讨论过复杂性对于软件开发的意义,这种做法会导致系统里充斥大量的类和各种ID,但它们并没有实际意义,会让系统设计步入复杂性的泥沼之中
正确方法
把客观世界拥有编号的事物建模为实体,如证件编号、订单编号、设备编号、快递编号等
注意
- 并非所有实体的编号都可以被用户看到,比如支付,你并不会意识到它有一个编号,而它可能恰恰是一个实体。这个方法可以尝试,但并不是一个通用的方法,必须查漏补缺
- 如何保证标识的唯一性往往也是个问题,比如学生编号在多大范围内起作用,是一个学校、一个地区还是一个城市
- 一个概念是否是实体,并不是天然属性,是完全依赖于在哪个业务中,取决于需求场景。人可能在多数场景中都是一个实体,但有时可能不是。一个概念是否是实体取决于系统的应用场景
验证
我们可以用以下标准来验证把一个领域概念建模为实体是否正确:
- 当它被替换为另一个具有同样属性值的对象时,影响业务的正确性
- 这些概念是否有状态的变化,比如:使用前,使用后,被确认
如果答案是其中一条或两条,则概念应建模为实体
案例理解
- 车票必须指定一个目的城市:车票目的地中的城市,不是实体。因为假设它是实体,两个名字都为北京的对象,即使ID不同,也不会影响任何逻辑,票价是多少还是多少,火车该怎么开还怎么开,而且它也没有什么状态
- 每个城市分配一个电话号码区号:城市电话号码管理里的城市,是实体。因为假设(我是说假设)两个城市重名,他们的邮政编码也不可能一致,肯定有城市ID
- 你的订单和其中的商品:
- 订单,是实体。即使是一个客户的相同的购买内容,并不意味着是一个订单,况且它还有各种状态
- 订单中的商品,不是实体,注意这里说的是订单中。同样配置的商品,对你而言并没有什么差别,不会影响业务的正确性。它们也没有状态,只有订单有状态
- 商家给你的优惠券和退货退还的钱:
- 优惠券,是实体。虽然优惠券都一样,但是它有状态,即使用前和使用后
- 退货退还的钱,不是实体。显然只要数额一样,不会影响任何东西,且它没有任何状态。但注意如果是退款,它就是一个实体,因为显然它有状态,即是否完成
- 你购买的100手股票:购买的100手股票,显然不是实体。今天买的股票和昨天买的并没有什么区别
5.生命周期
创建
介绍
实体的创建指从无到有生成一个实体,并为其赋予唯一标识的过程
方式
- 构造方法
- 静态工厂方法
- Factory
- Builder
- 等等
注意
- 实体的创建必须是原子的:不管通过何种方式创建实体,创建完成的实体业务对象必须包含其必须的属性,并且必须符合业务规则。在创建实体的过程中,如果任何必须的业务规则都得不到满足,则创建过程必须终止
- 必须给实体唯一标识:业界有很多唯一标识生成方式,需要给一个
重建
介绍
实体的重建是指实体已经被创建了,只不过暂时被持久化到数据库而不再内存中,需要通过其唯一标识重新加载到内存。这个把实体重新加载到内存的过程就是重建,重建实体往往通过Repository完成
注意
重建实体的过程是面向聚合根的,因为只有聚合根才会拥有自己的Repository。非聚合根的实体的重建过程只是聚合根重建的一个子过程
流程
Repository加载聚合根时,先通过ORM组件将数据库记录读取为数据模型,再根据数据模型携带的状态;创建聚合根并完成赋值
错误的实践(一)
许多ORM框架提供了将领域模型(实体或值对象)直接映射为数据模型的能力,通过这些ORM框架可以直接将领域模型持久化到数据库。不推荐这种方式,原因有以下两点:
- 领域模型职责不单一:领域模型既是业务承载着,又要被迫参与持久化的过程,导致领域模型的很多字段被加了@ID,@Column等ORM框架注解
- 领域模型被数据库设计绑架:在设计数据库时要考虑领域模型的实现逻辑,再进行领域建模时又要兼顾数据库设计;左右为难
错误的实践(二)
有的领域驱动设计实践者会将聚合根的创建和重建过程都放到Repository中。这是不合适的,原因是:
- 实体(此处为聚合根)的重建和创建是不同的概念;创建实体时Factory不需要通过数据模型获取数据,直接操作领域模型(实体和值对象)即可,并且创建的过程需要为实体生成唯一标识
- 重建实体一般发生在基础设施层,Repository需要了解如何将数据模型拼装为领域模型,由于此时实体已经有了唯一标识,因此不需要生成唯一标识
6.形态
代码
在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在DDD里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现
运行
实体以DO(领域对象)的形式存在,每个实体对象都有唯一的ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的ID,它们依然是同一个实体。比如商品是商品上下文的一个实体,通过唯一的商品ID来标识,不管这个商品的数据如何变化,商品的ID一直保持不变,它始终是同一个商品
数据库
与传统数据模型设计优先不同,DDD是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象
在领域模型映射到数据模型时,一个实体可能对应0个、1个或者多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。在某些场景中,有些实体只是暂驻静态内存的一个运行态实体,它不需要持久化。比如,基于多个价格配置数据计算后生成的折扣实体
而在有些复杂场景下,实体与持久化对象则可能是一对多或者多对一的关系。比如,用户user与角色role两个持久化对象可生成权限实体,一个实体对应两个持久化对象,这是一对多的场景。再比如,有些场景为了避免数据库的联表查询,提升系统性能,会将客户信息customer和账户信息account两类数据保存到同一张数据库表中,客户和账户两个实体可根据需要从一个持久化对象中生成,这就是多对一的场景