阅读 57

DDD 实践手册(3. Entity, Value Object)

上一篇我们介绍了如何在 Clean Architecture 与 DDD 的框架内划分一个项目的层级,而本篇文章中我们会聚焦在整个分层架构的核心部分,领域层中的关键概念: Entity(实体),Value Object(值对象)。

Entity 与 Value Object

当采用面向对象的设计方法对系统进行建模时,我们需要做的是从业务需求中找到那些关键的「业务对象」,而这些业务对象也是 DDD 中 Entity 与 Value Object 的基础。我们先来看一下 Entity 与 Value Object 有什么区别。

Entity 应该是我们在日常分析过程中最熟悉的部分,它也是业务逻辑的核心体现。它应该具备以下的特性:

* Entity 应该具唯一的「标识」
* 相比 Entity 所拥有的数据属性,我们更关注的是它的唯一「标识」
复制代码

看一下我们的周围,我们的世界中充满了各种各样的 Entity。例如汽车就是一个 Entity,而它的唯一标识是「发动机编号」。iPhone 手机也是一个 Entity,「设备编号」则是它的唯一标识。需要注意的是在不同的业务场景中,同一个 Entity 的唯一标识是可能发生变化的,例如作为自然人,我们本身就是一个 Entity,在某些场景下,「身份证」就能作为我们的唯一标识。但是在另一些场景下则可能需要姓名,身份证,银行预留手机号这三个元素组成我们的唯一标识。

与之对应的 Value Object 顾名思义,关注的是数据,因为它并没有唯一标识,如果两个 Value Object 的数据都一样,那么我们可以认为这两个 Value Object 就是同一个对象。反观 Entity,差异就很明显,两个相同数据属性的 Entity 不一定是同一个对象,应该查看它们的唯一标识。例如有着相同姓名的「张三」两个人,就是完全不同的两个人,因为他们的身份证号码是完全不一样的。

项目中大家一定接触过 Code Table,也就是俗称的「码表」,例如存放性别类型的「男」,「女」等,这就是一个典型的 Value Object。

我们再来总结下 Value Object 具备的特性:

* 没有唯一标识
* 我们更加关注于它的数据属性
复制代码

在此基础上我个人会再引申两个特性,具体的原因之后会详细说明:

* Value Object 不会「单独存在」,而是附属于某个 Entity
* Value Object 的生命周期会与所附属的 Entity 绑定在一起
复制代码

最后需要注意的是,相同的对象在某个业务场景下是 Entity,而在另一个场景下可能就是 Value Object 了,具体的例子我会在后面解释。

系统实现

了解了上述概念之后让我们看看如何在代码层面实现这两者。

有许多面向对象的设计理论可以用来指导我们发现业务需求中的 Entity,我觉得在前期不用太纠结于是否会遗漏某些 Entity,将专注点更多的集中在业务的核心流程上,特别是那些专业的业务术语。同时需要耐心分析的是 Entity 之间的关联关系,例如是 1 - N,还是 N - M。

结合 Eric Evans 书中的例子,我们分别看一下 Entity 与 Value Object 是怎样的。

上图是汽车作为 Entity 的类图,可以很清楚的看到,汽车本身有许多的属性,例如颜色,座位数量,舵位(左舵车还是右舵车),我们将「生产序列号」作为它的唯一标识。同时它关联的两个对象,一个为发动机,另一个则为轮胎。同样的,发动机也是一个 Entity,它有着排量,生产日期等属性,而它的唯一标识则是发动机编号。不同的是汽车所关联的轮胎对象,它是一个 Value Object。汽车的例子中我们并不关心轮胎是否具有业务的唯一性,我们认为凡是具有相同品牌与尺寸的轮胎都是相同。接着让我们看一个轮胎作为 Entity 的例子。

假设我们正在处理一个更换轮胎的业务流程,那么我们就需要区分每条轮胎之间的差异了,因为汽车所使用的四条轮胎很可能具备相同的属性,但是在修理过程中更换为另一条轮胎,所以我们必须能够区分彼此的不同。如上图所示,轮胎的类中还是具有品牌与尺寸的属性,但是作为 Entity 多了一个生产编号作为它的唯一标识,而它也关联了另一个 Entity,即生产厂商。

如果你依然有些迷惑,我们直接看一下这两种对象模型在数据库中的展现形式:

从数据库表结构的角度来看就很容易理解,Entity 有着自己的业务主键(实际项目中我推荐使用逻辑主键),而 Value Object 往往拥有一个指向所属 Entity 的外键,但是自己没有所谓的业务主键。图三则是 Value Object 的另一种映射方式,不用专门的表去映射一个 Value Object 对象,而是用 Entity 对应表上的几个字段(Car 表中 tier 开头的字段)。

Persistence Object - 持久化对象

上面提到了 Entity 与 Value Object 在数据库中的表现形式,现在我们再向上看一层,在 Java 中他们的存在形式又是如何的呢?此时我们有两个选择,是否需要引入 Persistence Object 即持久化对象(PO)。

PO 的概念是从 Hibernate(JPA) 等 ORM 框架中产生的,PO 是由这些框架管理的,数据库表在面向对象中的映射。我们先来看如果引入 PO 之后的好处与问题,参考下图:

引入 PO 的好处在于将 Entity,Value Object 等对象与持久化框架解耦,只需要使用 POJO 就能实现 Entity,Value Object,而无需引入第三方的接口。事实上 Eric Evans 的书中也提及到 Domain 层的对象应该保持简单,不依赖于任何的第三方外部框架。同时它还带来了额外的灵活度,可以按照不同场景的需求从持久层读取数据后组装不同的领域对象。

这也会带来另一些问题,其一是我们需要额外编写一部分 Entity,Value Object 等领域层对象与 PO 的转换逻辑。其二是在进行领域层对象组装时,需要完整读取所有 PO 的数据,不能进行延迟加载的优化,某些业务场景下可能存在隐藏的性能问题。

第二种方式是使用 PO 实现 Entity 与 Value Object,这也是一般项目,或是 Hibernate,JPA 等推荐的方式。它带来的优点很明显,可以省去一层抽象,不用编写那些冗繁的数据对象转换代码。这种方式的限制同样明显,首先是领域层的对象会依赖于某个具体的持久化框架(需要增加特定的 annotation),其次是在引入 Aggregate(聚合)后,不同业务场景,或是 Bounded Context(限界上下文)中,如果需要不同粒度的 Entity 映射相同数据库表时就会变的很麻烦,这个例子我会在介绍 Aggregate 时具体描述。

从项目实施的经验上来看,我更加建议分开 Entity,Value Object 与 PO 的关系,毕竟数据转换代码是可以通过框架减少很多,性能也是可以优化的,但是依赖关系在项目变得越来越庞大之后是没有那么容易解开的。

小结

我们介绍了 DDD 中 Entity,Value Object 的概念与具体实现,下一篇会更加的深入介绍 Entity 的一种特殊形式,Aggregate(聚合),以及 Entity 的整个生命周期的管理,Factory(工厂) 与 Repository(仓库)的概念与实现。

欢迎关注我的微信号「且把金针度与人」,获取更多高质量文章