滚动:《领域驱动设计》:从领域视角深入仓储的设计和实现

简介: 《领域驱动设计》中的Repository(下面将用仓储表示)层实际上是极具有挑战性的,对于它的理解,也十分重要。本文讲大部分内容都在众多前辈理论基础上,从一个崭新的领域视觉开始探索,并结合自己的实践感悟进行细致的解析。同时本文不仅仅是DDD前辈的搬运工,也创新提出了仓储实体转移的概念,可以提供给读者思考是否在自己场景中可以用到这种模式。即使读者对仓储已经有很深的了解,我也觉得本文会对你有新的阅读体验。

一、前言

“ DDD设计的目标是关注领域模型而并非技术来创建更好的软件,假设开发人员构建了一个SQL,并将它传递给基础设施层中的某个查询服务然后根据表数据的结构集取出所需信息,最后将这些信息提供给构造函数或者Factory,开发人员在做这一切的时候早已不把模型看做重点了,这个整个过程就变成了数据处理的风格 ”——摘 Eric Evans《领域驱动设计》


(资料图片仅供参考)

《领域驱动设计》中的Repository(下面将用仓储表示)层实际上是极具有挑战性的,对于它的理解,也十分重要。本文讲大部分内容都在众多前辈理论基础上,从一个崭新的领域视觉开始探索,并结合自己的实践感悟进行细致的解析。同时本文不仅仅是DDD前辈的搬运工,也创新提出了 仓储实体转移 的概念,可以提供给读者思考是否在自己场景中可以用到这种模式。即使读者也对仓储有很深的了解,我也觉得本文会对你有新的阅读体验。

导读:

本文首先从聚合根的生命周期和生存环境出发,引出了Repository概念,并说明其本质是管理中间过程的 集合容器 (2.1节); 根据集合容器的概念,在领域角度去挖掘出Repository的职责,并提出了 仓储实体转移模式 用作对不同仓储实现的对比标准(2.2节); 然后从实现例子出发,介绍了一种纯内存实现的仓储,用作体现仓储最佳实现(3.1节); 继续从实现例子出发,介绍了关系型数据库下的仓储特点,并描述 面向持久化的仓储 的特点(3.4节);

二、概念剖析

DDD作者在介绍仓储模式的时候,谈到了大部分技术的过程会入侵领域模型,让开发人员迷失,本文反其道行之,读者可以先假设内存是无限大的,便于我们先关注模型再讨论技术实现,然后我们先从DDD中的重要概念 聚合实体 的领域模型使用出发,挖掘出仓储的本质特征和与之相关领域概念,然后再从本质特征,指导如何实现仓储。

2.1 聚合实体

服务于实体的集合容器 :说到仓储我们必须要先讨论聚合(聚合是由实体和值对象组成,其中有一个实体为聚合根,后面提到聚合实体即聚合根),仓储必然是为聚合实体服务的,值对象则不必要。那我们的实体为何需要仓储呢?这得从实体的整个生命周期说起,我们先总结一下DDD中聚合实体的特点:

标识:实体具有唯一标识,这个唯一标识使得实体和值对象区分开来; 状态:实体是具有可以被改变的状态,因此聚合实体无法被静态描述; 生命周期:实体拥有生命周期,从实体的创建,到实体的状态的终态; 生存环境:实体的活动存在于各个上下文中的领域服务或者应用服务中,其中分用例过程和中间过程; 用例过程:只要在执行用例过程的时候才需要实体的存在,其他时候,实体生命周期并没有结束,而是处于中间状态; 中间过程 :当没有任何用例在处理一个实体的时候,实体消失了吗?没有,它仍然存在生命周期内,这个时候我们认为实体正处在一种中间过程。

其中最重要的就是实体会存在于各个上下文中的用例运行过程中,之外的都会存在于一个中间过程中,我们用图示来进行中间过程的描述。

检索聚合根 :在解决空间的运行态中,用例调度者(执行者、线程)要么新建聚合实体,要么获取中间过程的聚合实体,创建新实体好说,但是中间过程的实体是如何获取到的呢?其实中间过程的实体,只能是经过查找到得到的,这是一个 检索 的过程。其中检索包括全体遍历(包括索引)和关联遍历,不管何种检索渠道,我们都要让Domain感觉到,检索回来的实体还是原来那个实体。

统一语言 :中间过程、用例过程,这些词领域专家、业务人员是听不懂的,中间过程也不在模型关注点上,但又是与模型有关联。所以我们在领域角度、统一语言角度,封装角度,这个中间过程都应该提出一个统一的领域概念抽象,屏蔽掉中间过程的细节,让领域专家能明白我们的意思。 仓储 (仓库,贮藏室,Repository),这个词就很适合,它类似一个帮你暂存物品的仓库,然后你可以在仓库中找回你要的物品。

但这个词本身不重要,重要的是领域专家能听懂仓储这个词的语义,并和技术人员统一,搭建一个沟通的桥梁。有关仓储的统一语言应该有以下几点:

放置:建立一个新的聚合实体,这是一个聚合实体生命的开始,在用例过程结束后,把聚合实体放到仓储中; 查找:把已经存在的聚合实体找出来,这是一个聚合实体的中间过程到用例过程的行为; 管理:它负责聚合实体的中间过程管理,并屏蔽掉中间过程的细节,向领域层提供统一的能力抽象,一些数据统计类的也可以在该范畴内;

集合容器 :为了方便地把处于中间过程的实体找出来,我们的仓储需要解决两个问题,第一个是如何放置实体,第二个问题是如何检索实体。

如何放置实体:为了方便管理,我们通常会采用分治把同一种类型的实体放在一起成为一个集合。相同类型和集合给了我们一个指导就是:仓储的设计应该是一个聚合实体类型对应一个仓储实体,具有一一对应关系,所以 仓储实体应该是一个保存相同类型元素的集合容器 ; 如何查找实体:我们知道实体具有唯一标识别,也具有其他特征属性,所以为了查找实体,我们应该通过实体的唯一标识或者特征属性去遍历查找,仓储应当提供这种功能,所以仓储应该针对聚合实体字段具有索引查找功能; 如何查找仓储:既然我们提到了需要用仓储来查找实体,那么我们又是如何查到仓储的呢?其实这个很简单,如果一个聚合实体类型只具有一个仓储类型,那么我们把仓储设计为单例的就可以了。

我们从领域模型的生存环境角度,引申出了仓储的必要性,并在统一语言的原则上,从它的必要性行为中挖掘出了仓储的特征,关注领域模型的仓储,应当让客户感觉模型就一直在内存中一样,最后我们总结一下仓储的本质:

一个聚合类型(也就是一个聚合根),最好对应一个仓储(这个不是绝对的); 一个仓储应该是单例的,便于先查到到仓储,再查找到聚合实体(当然也不是绝对的); 仓储应该是一个集合的抽象概念,并且负责屏蔽中间过程,包括其中的实现细节,如持久化和重建, 它最好能让客户感觉它似乎就一直在内存中一样 ; 仓储作为聚合实体的集合,应该具有检索实体的功能,如果从技术角度看,那么将一直持有聚合实体引用;

2.2 仓储职责

仓储与统计 :在我们关注领域服务的时候,会有部分统计的领域逻辑可以归纳到中间过程管理中,例如我要根据某个聚合根的个数进行更新另一个聚合,仓储也应当封装这部分逻辑,主要是考虑到以下几点:

我们的一个用例服务中很可能不需要使用聚合实体本身,而仅使用到符合某种条件的聚合的数量,因此我们没必要查出聚合实体进行统计; 具体的基础设施数据库实现,对统计性能有着显著的性能优化,为了使用这些中间技术的优点,把统计这种细节的操作委托给仓储是一个很好的选择。 统计和查询有很多时候的应用场景是不修改聚合根状态的,所以这种情况你可能没必要使用仓储完成这件事,CQRS的思想要求我们去分离查询,建立查询模型,所以建立一套查询模型去做这件事是一个好的解耦实践。

仓储与规格 :上面提到仓储应当具有检索功能,检索必然需要一些聚合实体的状态字段作为入参,最好的直接检索是通过实体的唯一标识别进行,但如果我们有大量不同的字段检索需求,为每一个需求在仓储建立一个这样的方法接口,必然让仓储变得臃肿。 规格 这个概念可以消除这种臃肿变得可能。我们抽象一个规格实体,然后把规格作为一个参数传给仓储,让仓储根据规格获取聚合实体,便可统一检索功能。对该模式敢兴趣的可以参考Eric Evans的《领域驱动设计》第9章:

规格是一个谓词,封装了业务规则,可以明确表达一个特定实体是否满足该规格标准; 规则是值对象,可以组合使用,其组合实现与SQL的拼凑非常契合,使得其十分适合应用在仓储; 规格的概念引入,使得我们对实体多种检索的需求过程做到了通用化; 好的规格实现,链式 API 调用,可以使得编程变得灵活,表达能力强流畅;

仓储与唯一标识 :上面提到,聚合实体具有唯一标识,其中唯一标识的生产方法也有很多种(如用户输入生成、分布式ID生成、数据库持久化时候生成),生成时机也可以在执行用例步骤之初,也可以在事务持久化的时候。在用例执行之初的情况下,我们其实可以让仓储封装这种生成唯一标识,或者直接让仓储提供新聚合的工厂方法,这种表达会更自然。

仓储生成唯一标识别:在利用数据库能力生成唯一ID的时候(例如TDDL的Sequence),因为仓储本身封装数据库细节,所以仓储可以单独提供这种功能,例如 DomainRepository.getInstance().newEntityId() 方法,返回一个由数据库管理的唯一ID。 仓储提供工厂方法:聚合实体的创建,不一定是由领域服务完成的,如果我们的聚合实体具有创建模板,那么我们可以假设仓储本身具有大量的新对象池待使用。所以可以这样创建实体:DomainRepository.getInstance().newXXEntity() 返回聚合实体(该方式Evric不推荐);

仓储与Resource :Repository通常被翻译为资源库,个人认为对比仓储,资源库的描述可能会让我们更多的把聚合实体看作为一种网络中可以唯一定位的资源(Resource)抽象。我们通常在网络术语中看到资源的概念,如URL中的R即资源,如REST架构风格(表现层状态转移)也会把对象当初是资源。如果从资源角度看仓储,就是实实在在的资源库:

作为Resource,我们通常会给它定一个URI(统一资源识别),用作全网唯一识别,但很少资源库会定义URI,因为实体唯一标识已经足够; 作为Resource,仓储一但持有了资源,那么就一直持有并跟踪资源,直到资源被删除; 作为Resource,仓储有时会被当作是对远程服务进程封装的机制,这个时候仓储有点像防腐层,但我不建议这样做(国内部分书籍有这种介绍);

介绍这种角度,只是想让读者了解各种一些方案背后的设计理念。后面介绍面向集合的仓储的时候,或者需要结合DDD和REST架构风格的时候,读者可以自行体会聚合实体作为Resource的意义。

仓储实体转移(创新) :现在我们讨论一个问题,当我们从仓储中获取到聚合实体之后,仓储是否还应该拥有该聚合实体?如果我们抛开计算机和技术概念,完全从问题空间出发,那么仓储是不再拥有聚合实体的:想象一下,一个仓库管理人员需要处理一个商品,当他从仓库获取到该商品后后,另一个人在仓库中还能找到这个商品吗?按照这种思维对仓储进行建模,仓储和聚合的关系可以明确为:

聚合实体一个时刻只能存在于一个用例过程或者一个仓储实例中; 聚合实体无法同时存在在仓储中和用例过程中; 聚合实体也无法同时存在于两个用例过程中;

如果我们在空间中对这个过程进行建模,可以描述为下图:

有人或者会觉得我对这个仓储的建模太较真了,因为我完全从问题空间角度看这个问题,但我提出这个的目的,只是想为后面的实践方案提供一个以问题空间为主的参考标准,突出在仓储选择不同实现的时候不得不屈服于技术的特性从而使得仓储的特性产生的差异。我会在每个实现中提出如果要抹平差异要怎么做,并给出可以应用的场景,读者理解这些差异后会对仓储有更深的了解,其中《实现领域驱动设计》中Vaughn Vernon提出的一种实现为 面向持久化的资源库 和这种问题空间角度其实是相通的,而Vaughn Vernon提出的另一种实现为 面向集合的资源库 和解空间看的角度是相通的。我暂且将仓储实体转移描述为一种模式(后面统一为仓储实体转移模式),在该模式下,仓储领域本质上,应该只有两种操作:

放置(put或save) :把聚合实体从用例过程,放置到仓储中,状态变为中间过程,用例过程中不再拥有实体; 获取(Take) :用例过程运行中,需要把实体从中间过程,转移到用例过程,完成这个操作后,仓储将不再拥有实体,我特别用take而不是find表达了这种思想。

大家可以对比数据库的操作 更新 和 删除 。这两个操作是带着数据建模的思想,我将会在下面关系型数据仓储中提及,让大家衡量要不要仓储增加这两种行为。同时也会介绍在关系型数据仓储实现和内存仓储实现如何改进为仓储实体转移模式,达到对比的目的。

点击链接查看原文,获取更多福利!

https://developer.aliyun.com/article/1050042?utm_content=g_1000361270

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

关键词: 中间过程 生命周期

推荐DIY文章
主机存在磨损或划痕风险 PICO4便携包宣布召回
穿越湖海!特斯拉Cybertruck电动皮卡可以当“船”用
vivoXFold+折叠旗舰开售 配备蔡司全焦段旗舰四摄
飞凡R7正式上市 全系标配换电架构
中兴Axon30S开售 拥有黑色蓝色两款配色
荣耀MagicBookV14 2022正式开售 搭载TOF传感器
it