乐趣区

关于后端:领域驱动设计从领域视角深入仓储Repository的设计和实现

简介:《畛域驱动设计》中的 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 表白了这种思维。大家能够比照数据库的操作更新和删除。这两个操作是带着数据建模的思维,我将会在上面关系型数据仓储中提及,让大家掂量要不要仓储减少这两种行为。同时也会介绍在关系型数据仓储实现和内存仓储实现如何改良为仓储实体转移模式,达到比照的目标。作为开发人员,咱们在利用 DDD,关注模型的时候隔离了两头过程,的确失去了以模型为关注点的概念设计,但咱们还须要兼顾技术的实现难度以及可行性,其实整个仓储的解决方案在细节中并没有那么简略,上面咱们开始沿着畛域模型剖析的论断,开始看技术实现的鸿沟。三、实现分析如果有无限大的内存,或者无需长久化的业务,DAO 层必然不存在,但仓储(汇合容器 + 检索的数据结构)是依然存在的。这就是为什么我认为,了解仓储的实质,不应该从技术角度思考,而是从畛域角度思。即便咱们对仓储在畛域上有简直固定的职责和性能,具体实现的仓储都很难满足其畛域模型角度的性能。在《实现畛域驱动设计》一书中,Vaughn Vernon 提出 2 种仓储的实现模式:面向汇合的资源库:面向汇合的仓储提出的是齐全依照汇合的理念去设计仓储,就仿佛它就是 Set 数据结构一样。所以他能主动去跟踪聚合实体的变动面向长久化的资源库:面向长久化的仓储,外围点是合并了插入和更新这两种操作,对立用 save() 操作齐全取代仓储旧实体使得仓储的性能更对立。这种数据存储(如 MongoDB 等文档数据库)通常称之为:面向聚合的数据库(Aggregation-Oriented DataBase)或聚合存储(Aggregation Store)。以上两种模式对仓储来说都没有对立,他们各有不同特点,面向汇合模式强调仓储始终放弃跟踪(援用),而面向长久化则强调采纳 save() 或者 put() 操作全量笼罩。本文的实现介绍角度不同,但成果差别不大,本文只对内存实现和关系型数据库实现做辨别,并心愿在对立的角度做了一些解读给读者参考。但我认为读者能够依据本人了解去偏重抉择本人的实现。3.1 内存仓储在《实现畛域驱动设计》一书中,作者 Vaughn Vernon 提出一种面向汇合的仓储,我认为这其实就是一种齐全面向内存实现的仓储形式,在这种形式中,咱们利用仓储治理聚合实体的生命周期两头过程其实和应用框架汇合(Collection)是一样的。我把书中的例子稍改变展示如下:public class CalendarRepository extends HashMap{

private Map<CalendarId,Calendar> calendars;

public CalendarRepository(){

this.calendars = new HashMap<CalendarId,Calendars>();

}

public void add(Calendar aCalendar){

this.calendars.put(aCalendar.getId,aCalendar);

}

public Calendar findCalendars(CalendarId calendarId){

return this.calendars.get(calendarId);

}

} 相熟编程的人员很简略就晓得这是怎么一回事了。这个实现也很能表白从畛域模型的角度看仓储应该是怎么样子的。我总结了该实现特点如下:仓储应该是一个汇合实例,而且无奈对仓储进行反复的搁置;从仓储获取的聚合实例,该当和搁置仓储的实例具备齐全一样的状态,在这里是原对象;如果在仓储之外对聚合实例进行了批改,无需“从新保留”聚合实例;这种仓储下的聚合实体,看起来更加像资源 Resource;抹去援用的翻新改良:Vaughn Vernon 的这个例子齐全解析了仓储应有的样子,但即便纯内存实现也不得不融入了实现的个性——仓储齐全持有汇合。这种持有援用个性简直对畛域无影响,但我还想试图把这种实现个性抹掉。比照 2.2 两头过程的仓储实体转移一大节中,当取出资源后,汇合不应该再领有聚合实体。所以依照这种思路进行,findCalendars 办法还应该加上逻辑移除 Calendar 聚合的实现,如上面代码所示。但这样齐全模仿有什么益处呢?这是一个好问题,因为咱们的抉择必须要衡量其中得失。持续往下看一下不这样做引起的并发抵触问题 ……public class CalendarRepository extends HashMap{
// 存聚合实体
private Map<CalendarId,Calendar> calendars;
// 标记实体被逻辑移除
private Map<CalendarId,Thread> calendarsTakenAway;

public CalendarRepository(){

this.calendars = new HashMap<CalendarId,Calendars>();

}

public synchronized void add(Calendar aCalendar){

this.calendars.put(aCalendar.getId,aCalendar);
// 移除逻辑删除
calendarsTakenAway.remove(aCalendar.getId)

}

// 留神咱们改了命名办法,变为了 take,获取,体现仓储不再领有实体
public synchronized Calendar takeCalendars(CalendarId calendarId){

// 如果曾经被取过,无奈再取
if(calendarsTakenAway.containsKey(calendarId)){return null;}
Calendar calendar = this.calendars.get(calendarId);
// 逻辑删除
calendarsTakenAway.put(calendarId,Thread.currentThread());
return calendar;

}

} 思考并发:在畛域角度,在同一个时刻没有有两个人能够同时在一个仓库中获取到同一件商品,但在计算机解空间中能够,所以计算机解空间会呈现并发问题。为了解决并发问题,咱们能够应用以下形式乐观锁:在一个调度者(线程)应用该聚合实体前,先对聚合实体进行加锁,其余调度者则无奈获取实体进行操作阻塞乐观锁:如果调度者发现聚合实体被锁了之后,则进行调度直到期待失去实体锁后持续;非阻塞乐观锁:如果调度者发现聚合实体被锁了之后,不期待锁,立刻返回做其余用例;乐观锁:一个调度者认为抵触可能性不大,所以能够先获取聚合实体进行事务操作,然而当它想把聚合长久化的时候,发现有人操作过这个聚合,则回滚本人所有的操作。采纳哪一种操作齐全取决于软件开发人员,这个时候要求咱们对程序架构设计和运作形式有着充沛的理解,然而咱们能够看到,其实用到了仓储实体转移这种齐全模仿实在的畛域问题空间的实现,刚刚好就是非阻塞乐观锁。只有是 findCalendars 办法删除找到的 Calendar 实体是原子性的操作,其余线程则无奈获取到实体,那么咱们便不须要思考从新设计一个新的锁计划。如果你不是为了性能等其余因素非要畛域模型斗争或者你刚好抉择的就是非阻塞性乐观锁,那么这种实现将会大大简化你的程序代码分量,也能让客户理解你的模型运作机制,使得该过程也做到了对立语言。即便是咱们罕用的乐观锁,在数据库仓储下仓储实体转移也十分实用。最初明确一下,做到对立语言,回归畛域实质的意义十分大。它是畛域驱动设计应酬软件简单之道的外围实践根底。它要求咱们抓住问题的实质复杂度,尽量排除因计算机技术计划引入的偶尔复杂度,从而实现软件的架构价值,获取久远的软件效益。3.2 关系型数据库仓储 DAO 和仓储思维差别:正如本文开篇中的第一段话所援用,咱们程序员通常会在实现技术的过程中,把关注模型的想法早早抛之脑后,这是能够了解的,咱们在入门该迷信所承受的根底学习让咱们的思维很大水平上固化为面向计算机技术的开发,却往往没留神到,软件工程的设计建模更应该关注的是模型,DAO 和仓储正是这两种差别的产物。本文不会解析 DAO 和 DO 之类的概念,因为读到这里的读者,对他们的理解该当是十分业余的。DAO 和仓储实现差别:先引出一个例子:咱们有一个主工作 TaskA 和两个子工作 subTaskB,subTaskC,这三个实体都有一个叫 state 的状态字段,咱们有一个业务规定是:所有子工作实体的状态都是 FINISHED,那么就把 TaskA 实体的 state 设置为 FINISHED。然而内部事件是一个一个子工作回传回来的,咱们接下来看不同思维的实现。面向数据的开发思维,应用关系型数据库实现仓储的时候,咱们对数据表有插入、更新、删除、查问四种次要操作,而且在面向数据模型开发的时候,服务类自身明确晓得本人是在做哪一步操作。所以面向数据模型的开发常常会写这样的代码:public class BusinessService {

@Resource
private TaskDao taskDao;

@Resource
private SubTaskDao subTaskDao;

@Transactional
public void onFinished(String subTaskId,String taskId){

// 查出所有子工作
List<SubTask> subTasks = subTaskDao.getAllSubTask(taskId);
// 找出回传的子工作
SubTask callBackTask = subTasks.stream()
  .filter(e->subTaskId.equals(e.getSubTaskId)).findAny();
// 更新子工作状态
callBackTask.setFinished(true);
// 如果所有子工作实现,更新主工作状态
if(allFinished(subTasks)){taskDao.updateStateById(taskId,TaskStatusEnum.FINISHED);}
// 更新一个字段
subTaskDao.updateStateById(subTaskId,TaskStatusEnum.FINISHED);

}

} 下面的代码,用例服务晓得本人要更新什么字段,并自行去做了更新,但当咱们关注模型思维用到仓储的之后,针对以上的性能实现用例服务就不应该关注到更新哪一个字段这个和长久化相干的操作,而是让仓储须要自行去比照,哪些字段变动了,而后更新到数据库中去,用例服务会是上面所示的样子:public class BusinessDomainService {

public void onFinished(String subTaskId,String taskId){

// 获取实体的时候记录快照
Task task = DomainRepository.getInstance().taskOf(taskId);
// 聚合实体负责业务逻辑
task.subTaskFinished(subTaskId);
// 仓储本人辨认到底哪个字段变动了,而后更新该字段(简称 diff)DomainRepository.getInstance().put(task);

}

}

public class Task {

private List<SubTask> subTasks;

private TaskStatusEnum status;

public void subTaskFinished(subTaskId){
  // 找出回传的子工作
  SubTask callBackTask = subTasks.stream()
    .filter(e->subTaskId.equals(e.getSubTaskId)).findAny();
  // 更新子工作状态
  callBackTask.setFinished(true);
  // 如果所有子工作实现,更新主工作状态
  if(allFinished(subTasks)){status = TaskStatusEnum.FINISHED;}
}

} 以上就是面向数据开发和面向畛域模型的仓储开发的差异。那么这样的例子应该抉择哪一种实现最好呢?这个问题不好答复,既然是 DDD 那只能抉择仓储,这根本波及的是零碎如何设计的问题。简略的零碎抉择面向数据开发是简略间接的。你应该在什么时候应用「畛域驱动设计」这种仓储设计思维,别忘记了它的作用:复杂性软件应答之道。简单的聚合根实体:如果你的数据字段是无限的,然而实体变动的规定是多种多样的,那么实现自动更新模式将失去益处。假如咱们一个实体有 20 个字段,那么咱们 diff 20 个字段的代码必然比写不晓得多少个由这 20 个字段组成的组合接口要强。另一方面,比拟可怕的是,有可能用例过程自身基本不晓得一个要更新的实体哪些字段产生了变动,为了阐明这些状况,咱们不得不提一下聚合根的另外一些特点。聚合外部一致性:聚合根的存在,最次要是的封装和治理聚合外部各种实体的关联和耦合,包含代码耦合和数据耦合,所以下面的 task 自身持有所有 subTask 的援用,而且负责 subTask 和 task 的 state 状态业务规定统一。此时,这个事务处理过程,就无奈感知 Task 封装的一致性逻辑是否由 subTask 引起了 Task 实体本身的状态变动成为 FINISHED,所以 diff 的实现就很有必要。

畛域服务的纯正性:如上图所示,因为设置 Task 的状态规定是由聚合根负责,所以畛域服务是不感知的,必须要靠 diff,然而如果把 diff 这个逻辑写在畛域服务中,不如把逻辑写在仓储中,因为咱们也不应该让畛域服务去关注一些技术上的逻辑,减少畛域服务逻辑的复杂性。其实这样做,刚好就是仓储自身的职责,封装 diff 后的仓储让畛域服务感觉到聚合实体始终在内存中一样。聚合根的重建工序:在 DAO 中,咱们能够间接不便从 ORM 框架中返回数据对象,然而聚合根却不能,因为聚合根是由多个 DO 组成的,咱们的长久化中间件(不论是 MySQL 关系型还是 MongoDB 文档型)无奈给咱们返回一个聚合根实体。所以仓储还得老老实实的把 ORM 中获取到的 DO 组装为 Entity 和 Value Object,且要保障查找到的实体是要和原来的实体一摸一样的。这意味着须要“重建”实体的操作;拆建规定(Convertor):仓储该当晓得怎么拆,就应该怎么还原,所以它应该有一套拆解和重建规定,并依据此规定进行还原,Convertor 是保护这种规定的一种工具,我倡议采纳这种命名类封装拆建规定事件溯源(Event Sourcing):还有一种重建工厂的实现是利用实体的快照 + 实体的畛域事件汇合回放来复原聚合实体,有趣味的同学能够理解一下事件溯源;聚合根与关联单例:关联单例是一种非凡的重建工序。我用一个畛域事件监听器来阐明,例如咱们的聚合根实体实现了观察者模式,聚合根为主题,外部持有一些单例监听器对象列表,其中一个监听器用作监听聚合根的状态变动发送畛域事件,那么这个监听器也应该让仓储负责拆解和复原。以上的几种个性,都意味着关系型数据库仓储的实现都会比较复杂。但这种简单换来的是咱们畛域模型的洁净,当软件系统的复杂度晋升,面向数据的开发所带来的偶尔复杂度是指数级别的,所以这个时候咱们就能感触到仓储的复杂性付出是值得的。最初咱们列举一下比照 DAO,仓储的毛病:实现简单:因为聚合的复杂性所以咱们其实现起来也十分艰难,其中最好模型能配合实现这种复杂性。犯错老本:正如 DAO 的某个接口只对一个属性更新,那么无论代码有何种 bug,最多只会写错一个字段,但仓储全量化更新后,咱们在未知状况下手一抖,那么将可能笼罩其余本应平安字段,所以这也进步了咱们的犯错老本。断言是解决的一种较好计划关系型仓储实现计划:仓储必须要让客户感觉它仿佛就始终在内存中一样;但下面提到的 Diff 逻辑让仓储的应用和实现变得艰难,设计者须要在整个上下文角度理解仓储的原理细节,因为要谋求性能和平安的实现,还要只针对曾经变动的字段更新,疏忽无变动字段。其中 Vaughn Vernon 在《实现畛域驱动设计》外面提到了两个办法,来解决这个问题:隐式读时复制:在查找聚合实体的时候,记录下聚合实体的所有状态,而后在更新的时候,用新状态 diff 旧的状态,只对特定字段进行更新;隐式写时复制:在查找到汇合实体的时候,仓储把聚合实体的更新操作隐式委派给仓储的某种机制进行,所以每次更新状态实体状态仓储都能跟踪到,并在这个时候对该值标记为脏数据,最初仓储在事务完结的时候把脏数据给刷盘。public interface TaskRepository{

// 相当于 findTask,获取到的 Task 会被隐式追踪复制
public Task taskOf(String taskId);

public void addTask(Task task);

public void removeTask(String taskId);
// 其余 / 统计 / 汇合操作等
//......

} 看下面代码,在获取办法 taskOf() 中,仓储负责开始对实体进行跟踪,因为外界调用方不感知仓储在跟踪实体,所以称之为隐式,咱们能够依据聚合的不同形成自行实现以上提供的两种隐式跟踪的计划的一种,如果是谋求性能那么写时复制比拟好,如果是采纳读时复制,那么 Javers 开源框架会是一个比拟好的抉择,但记得肯定要做好单测。以上两种计划其实都是对实体进行状态跟踪,但要留神的是在介绍这两种计划的时候,Vaughn 是打算让仓储往面向汇合仓储的思路走的(该办法被他归到面向汇合一章)。尽管以上两种隐式计划是十分好的实际,但我认为还是能够像在面向内存仓储一节提到的一样,持续引入翻新改良为仓储实体转移模型,当初咱们看一下关系型数据库仓储该如何应答这种模型。抹去跟踪的翻新改良:咱们下面提到了,仓储实体转移模式下,仓储实则只有两种次要操作,一个是搁置聚合实体,一个是获取(Take)聚合实体。获取到实体后,仓储将不再领有实体管理权限。在面向内存的仓储实现中,咱们只需在 take 办法中 remove 掉实体即可。然而长久化下的这种仓储模式该如何实现、又有什么特点呢?很简略,只有咱们在原来的根底上,让仓储把插入和更新(即下面的跟踪)操作封装为一个操作 put(也能够用 save),而后让 find 操作不变,间接命名为 take,让畛域服务认为仓储实际上曾经没有实体即可实现仓储实体转移模式,解析如下:畛域服务视觉:在获取(take)到聚合实体后,畛域服务能够认为仓储中的聚合实体是不存在的(即便仓储没有删除聚合实体);合并插入和更新(全笼罩):仓储没有所谓的更新操作,只有间接搁置聚合实体到仓储中,能够让仓储判断该插入还是全量更新(其实和用隐式跟踪实现局部更新差异不大,隐式跟踪更平安但多一个复制操作),或者咱们间接一点,齐全删除实体后再次插入或者全笼罩实体;删除:不论是否改良模型,当聚合实体生命周期完结都须要去真正的删除实体,这一点的确不好对立;乐观锁:咱们能够在实现的时候在关系型仓储中采纳乐观锁保障一个聚合实体不会存在于不同的畛域事务中。因为乐观锁只会让其中一个胜利;

在 Vaughn 的书中介绍,隐式读时 / 写时跟踪是做成面向汇合的 Repository,而另外用面向聚合的数据库(Aggregation-Oriented DataBase)来表白他的面向长久化 Repository,不晓得读者是否能 Get 到其实关系型数据库实现的仓储实体转移模式,正是关系型数据库下的面向长久化的 Repository。长处:所以它最大的长处就是无需跟踪实体,而是以转移的聚合实体为主;毛病:因为仓储实现要全量笼罩整个聚合状态,所以只适宜用在类文档数据库,对于关系型数据库则须要简单的隐式读 / 写跟踪了;关系型仓储总结:但的确不同的实现仓储体现出了不同的特点,所以不论用何种实现,咱们都须要理解仓储的应用办法,不然是无奈正确应用仓储的。上面给一个图大略形容一下关系型数据库长久化仓储的性能和内部结构:拜访对象 DAO:能够封装一层 Mapper,或者其余 ORM 框架,提供 DO 以及其余统计数据;Convertor:保护拆解规定和重建规定,同时复制聚合根监听器的一些组装;DO:数据对象,个别和关系型数据表一一对应;隐式状态跟踪:实现一套隐式读时复制和隐式写时复制状态跟踪的逻辑;

当性能不是很重要而且代码比拟器重品质的时候,我比拟举荐举荐在畛域服务完结之前,都要把聚合实体回归仓储,而后用乐观锁把整个聚合实体替换掉仓储实现中的聚合实体。在开发标准束缚、对立语言闭环的状况下,咱们有了这条默契的规定,就不必放心这种漏掉长久化实现的问题,也无需思考咱们到底是插入还是更新。3.3 仓储的架构仓储层(资源层):咱们提到,两头过程是不归畛域模型关注的,咱们屏蔽了两头过程提供了仓储的畛域概念,那么显然仓储是畛域模型关注的,这就波及一个耦合以及依赖的问题。其中最天然的依赖就是咱们的畛域服务,要依赖仓储,而仓储要依赖数据库、内存等具体的实现工具去做真正的两头过程状态保护(长久化),如下图所示(图中连线代表依赖关系):

如此,在代码实现上,必然很容易让畛域模型对数据库、内存等这里基础设施的代码产生依赖,从而让基础设施的概念入侵到畛域模型变得容易。咱们习惯于面向数据和过程的开发,当这类代码和畛域模型的代码界线变得没那么显著的时候,聚焦于模型也容易被毁坏,倒置依赖和整洁架构分层给了咱们解决这个问题很好的实际。咱们能够把仓储的行为形象为根本的接口,而后利用管制反转,把实现该节点的仓储注入畛域模型的运行态中。实现了倒置依赖的依赖图如下:

利用了依赖倒置,把所有的仓储都在一个命名空间(模块)中治理,就造成了咱们熟知的仓储层(也叫资源层):四、结束语对 Repository 的认知其实和对 DDD 思维的意识是对立的,他们都是从领域专家角度去对解决方案进行建模。仓储为聚合根在畛域常识和工程常识之间做了隔离,并为技术实现提供了对立的概念形象。这样的模式和例子在 DDD 中是常常有的,例如:防腐层也是其中的一种,他们都是为了放弃畛域模型的纯正性作出了本人的致力。最初因为篇幅问题简要提一下仓储的一些我还能想到的关注点:仓储与事务:聚合根是事务批改的根本单元;所以仓储其实也是暗藏着一个事务原子化的能力。咱们通常数据库事务的实现要管制在应用层,但有时候会遇到大事务问题或者两阶段提交的问题,所以有极其状况下把事务用一个畛域概念进入畛域层,从而让仓储层的实现来反转管制事务也不失为一个好抉择。这种突破准则的事件也要求咱们了解准则。仓储与值对象:值对象能够很简略,就一个数字,也能够很简单,如一个齐备的 Domain Primitive 概念。咱们的实体领有值对象,所以 Repository 也是要负责值对象的长久化,这点的解决也是十分值得大家去留神的点。读者在实战中解决的值对象的时候更须要丰盛的教训去取舍设计方案。仓储的设计和实现非常的简单,咱们很难在节奏比拟快的开发迭代中去实现业务不关注的这种设计形式,这或者要求咱们在每一次不同的迭代中去缓缓实现一个仓储。这个时候代码实现的仓储有多俊俏不重要,或者重要的是你心中有一个成型的仓储,它始终会跟着你的每一次改良被积淀、演进。这就是为什么咱们要去了解仓储存在的意义和实质,开发者如何去对待一个零碎的各个构件,最终零碎就会被开发成什么样子。参考书籍:《畛域驱动设计》Eric Evans [著]. 赵俐 [译]2016.. 人民邮电出版社《实现畛域驱动设计》Vaughn Vernon[著]. 滕云 [译].2014. 中国工信出版团体原文链接:https://click.aliyun.com/m/10… 本文为阿里云原创内容,未经容许不得转载。

退出移动版