DDD 实践手册 (5. Factory 与 Repository)
之前的两篇文章中我们讨论了是领域的对象的核心概念,即什么是 Entity 实体与 Value Object 值对象。以及如何使用 Aggregate 聚合模式来封装 Entity,以保障它数据完整性。而本篇文章会讨论有关领域对象的最后一个部分,如何使用 Factory 工厂与 Repository 仓储模式来管理 Entity 的生命周期。
关于如何创建对象的思考
在 Java 语言中我们可以很容易的使用 new
关键字配合构造函数来创建一个对象。但是这样的做法比较简陋,容易存在一些潜在的问题。这也就是为什么在 Java 的经典著作 「Effective Java」中的开篇就建议大家尽量使用 Factory 工厂模式来创建对象,而不是直接使用 new
关键字。
对于领域对象亦是如此,一个领域对象应该由数据与方法组成,在领域对象被创建时,如何对数据做必要的初始化,以及 Entity 之间的关联关系都是值得思考的。
假设业务系统有个前端用来录入保险被保人的详细信息,而在后端负责处理业务的逻辑中需要将前端页面填写的数据存入关系型数据库中。按照之前文章中提及的架构,从前端的 HTTP 请求中获取相关的参数后需要在 application
那一层将数据转化为领域对象,一般是作为 Aggregate Root 的 Entity。
那么我们该如何创建一个新的 Entity 呢?当然使用 new
在功能上是可行的,但在实际项目中会有两个问题。
缺少封装
在 application
层中进行数据装配时会产生许多的重复代码。例如上述例子中初始化被保人信息时,需要将被保人作为一个 Aggregate,而它会关联保险产品,付费信息,联系方式等 Entity 或是 Value Object,如果每次在初始化被保人这个领域对象时,可能都需要写一堆的像下面这样的 getter / setter 方法:
更糟糕的是这种通过 getter / setter 方式初始化数据很容易造成开发人员的错漏。当一个对象结构较为复杂时,开发人员很容易遗忘调用某个 setter 方法而导致创建了一个数据不完整的 Aggregate。
可能稍好一点的方式是不用 getter / setter 方法,而是将这些关联对象作为构造函数的参数传进去,在构造函数中完成数据结构的装配。这从某种程度上解决部分的问题,但是它依然存在缺陷。
缺少业务语义
如同上面所说的,我们可以通过构造函数来保证 Aggregate 数据初始化的完整性。但是这种方法带来了第二个问题。想一下,在不同业务场景下 Aggregate 初始化所需要的参数可能是不同的,如果通过自定义构造函数的方式来控制数据初始化,那就需要定义多个参数不同的构造函数,即所谓函数重载(Overload)。例如这样:
上述两个构造函数有着不同的参数列表,应该在不同的业务场景中使用,但是因为方法名是相同,无法表达业务含义,因此开发人员还是无法确定到底应该使用哪一个都早函数进行数据初始化。
Factory Pattern
为了解决这个问题,DDD 比较推荐的一种方式使用经典的 Factory Pattern(工厂模式)。工厂模式作为最简单的设计模式之一,被广大的开发人员所熟知,在 GOF 的书中,工厂模式也是第一个介绍的设计模式。简单来说,工厂模式通过一个特定方法,封装了对象数据初始化的逻辑。而这个方法其实就是个普通的方法,因此可以自由的定义方法名,而不必像构造函数那样受限,所以可以自由的表达业务含义。在项目中具体的实现方式也有两种选择,让我们依次来看一下。
由领域服务提供的 Factory Method
我们之前在分层架构中提到过领域服务的概念,如果说领域对象从某种程度上代表了领域知识中的名词,那么领域服务就对应了动词。我们可以在领域服务中定义所需要的方法来返回一个 Aggregate。
上述的代码中我们定义了一个领域服务类,用来实现新保单承保的逻辑,其中的方法 createInsured
会返回一个 Insured 的实例,这就是我们定义的用来创建 Aggregate 的工厂方法。通过这样在领域服务中定义专门的方法,可以很好的封装领域对象的初始化逻辑,保证数据完整性的同时也不丢失业务含义。
由另一个 Aggregate 提供的 Factory Method
除了在领域服务上定义相关的工厂方法之外,在 Aggregate 上也能定义专门的方法来管理另一个 Aggregate 或是 Entity 的初始化。我们通过一个保险业务上的例子来说明这种情况。当被保人发生意外,如果在保险单的保障范围内,可以申请理赔。在申请理赔时需要录入许多事故相关和保险单相关的信息,因此可以将理赔申请设计为一个 Aggregate。而初始化这个 Aggregate 的方法可以交给另一个 Aggregate,即保险单的 Aggregate。具体代码可参考如下:
上面的方法很好理解,在 Policy
上有个方法,applyClaimWith
,它接受一个事故信息 Accident
对象,返回另一个 Aggregate ClaimApplication
。当采用这种解决方案时,我们需要更多的分析领域对象之间的关系,在合理的对象上定义工厂方法,切忌在一个 Aggregate 上定义过多的工厂方法,这样也就丢失了相关的领域知识。
Repository Pattern
工厂模式能够帮助我们控制对象的初始化,这些对象创建之后还是处于内存之中,而作为一个业务系统不可避免的需要一种持久化的机制,能够将这些领域对象对应的数据存储起来,最常见的一种持久化机制就是关系型数据库。也由此我们可以看到,初始化对象数据来源并不仅仅来源于外部的输入,还可能来源于某种持久化机制。所以我们会引入领域对象生命周期的概念,参考如下的图片说明:
一些开发者可能会把 Repository 与 DAO 混淆在一起,由于 Spring JPA 这样的框架在命名方面把两者交织在一起,更加容易加深大家的误解。Repository 从字面以上来看更加偏重业务的含义,作为一个「仓库」它所要做的是将领域对象重新拿出来,但是不必关心底层的细节。例如我们是使用一种关系型数据库,还是 NoSQL 数据库,作为领域层其实是不关心的,它们关心的是领域对象是否被正确的还原出来。而 DAO 在实际项目中往往会更底层些,它抽象的是不同关系型数据库的异同,你可以使用 MySQL,也可以使用 Oracle,但是对于 DAO 层暴露的接口应该是相同的。我们来看一个具体的例子。
以上的代码中我们对两个领域对象,Insured
与 Product
定义了两个 Repository
接口,用以与某种存储机制进行交互。接下来看我们的实现。
我们使用关系型数据库存储 Insured
的数据,同时为了保证不耦合到特定的关系型数据库,我们定义了一个额外的 DAO 抽象类,然后提供了基于 MySQL 实现的具体类。而在 Product
这方面,我们更希望使用 MongoDB 这样一个 NoSQL 存储数据,因此我们直接使用了一个具体的类实现了 ProductRepository
的接口。但是这两个接口在领域层暴露的几口都是一致的,所以需要牢记的是 Repository
是属于领域层的,而具体存储机制的实现,无论是 DAO
还是其他的实现,都应该属于 infrastructure
层,属于具体的实现机制。
小结
这篇文章中我们谈论了领域对象生命周期的概念以及如何使用 Factory 与 Repository 模式来管理,封装领域对象。如何在一个业务规则复杂的系统中保证领域对象数据的完整性是非常重要但也是困难的,因此希望通过这次的文章你能从中获得一些启发。下次我们会讨论 DDD 中另一个很重要的概念,限界上下文和对应的代码实现,希望你不会错过。
往期推荐
https://xie.infoq.cn/article/bc2245284b73ebca7b14308dc DDD 实践手册(1.Get Started)
https://xie.infoq.cn/article/2bc22422a8ace939e40125882 DDD 实践手册(2. 实现分层架构)
https://xie.infoq.cn/article/8fedd10940281f69bd228e17c DDD 实践手册(3. Entity, Value Object)
https://xie.infoq.cn/edit/0ad9a63950d16613fd59fc69f DDD 实践手册(4. Aggregate — 聚合)
版权声明: 本文为 InfoQ 作者【Joshua】的原创文章。
原文链接:【http://xie.infoq.cn/article/17a517b0efc678c904d2f88d5】。文章转载请联系作者。
评论