如何运用领域驱动设计 - 存储库

概述

在上一篇文章中,咱们已经了解过领域驱动设计中一个很核心的对象-聚合。在现实场景中,咱们每每须要将聚合持久化到某个地方,或者是从某个地方建立出聚合。此时就会使得领域对象与咱们的基础架构产生紧密的耦合,那么咱们应该怎么隔绝这一层耦合关系,使它们自身的职责界限更加清晰呢?是的,这就要用到咱们今天要讲的内容 - 存储库。在不少地方,咱们喜欢叫它为仓储,特别是在现有的AspNetCore应用中,大量的应用都在引入Repository这种东西。那么究竟什么是存储库呢?咱们如今的使用方式是正确的吗?它在领域驱动设计中又扮演着怎样的角色呢?本文将从不一样的角度来带你们从新认识一下“存储库”这个概念,而且给出相应的代码片断(本教程的代码片断都使用的是C#,后期的实战项目也是基于 DotNet Core 平台)。git

直接看东西

直接看东西

“少啰嗦,直接看东西”。是的,在本次的文章中,竟然!竟然!竟然! 附带了Github的代码。本次代码实际上是演示工做单元的实现,可是它确实又结合了存储库的一些内容,因此就在这里提供给你们参考。github

GitHub 地址,点击直达哟数据库

这是一个工做单元的超简易版本,您能够在github中看到它的描述和简介,这里我就再也不重复了。下一次的文章会对工做单元的实现进行解析和优化,可能它就不属于 《如何运用领域驱动设计》 系列的正传系列了(算个番外吧 ( ̄▽ ̄)")。因此为了您不错过这一部分,能够点击博客园右上角的关注,有了动态以后就可以第一时间收到啦!c#

哦,对了!在Github代码中,您可能会看到一个叫作MiCake(米蛋糕)的东西,它是咱们一步一步实现的DDD组件,它会让您的 aspnet core 应用更轻松的融合DDD的思想,而且它包含了咱们该系列博文中所提到的全部战略组件,以及它们之间的约束和处理。设计模式

被普遍使用的仓储

是的,说存储库模式您可能还不能一下想到这是个什么东西,可是一说到仓储,您可能就会有一种豁然开朗的感受:“哦!就是这个东西呀!”。回顾一下,您现有的AspNet Core项目,是否已经引入了一个叫作Repository的对象,而且它为您提供了与数据基础架构交互的方法。架构

仿佛从某一天开始,以往咱们使用的BLL,DLL这种东西就逐渐开始消失了,替换它们的是一个叫作Repository的东西。特别是从传统的AspNet演化为AspNetCore的阶段,大量的应用都开始使用仓储了,即便您在使用相似于EF这样的ORM框架。mvc

仓储是反模式吗

关于存储厍模式存在很是多的误解和混淆,许多人认为它是多余的仪式以及没必要要的抽象,它隐藏了底层持久化框架的能力。特别是当您正在使用相似于Entity FrameWork Core这样的ORM框架的时候,您是否发现明明EFCore直接就能够实现的东西,为何我又在它的基础上套了一层,并且这一层中我并无执行任何逻辑,只是简单的调用DbContext(EF中的数据上下文)这种东西。那为何我不能直接调用DbContext呢?是的,这样的疑问相信不止不少同窗都遇到了。因此在微软EF Core 3.x的官方教程中,提到了这样的一句话:框架

Ef 中的解释

该内容位于 ASP.NET Core 官方教程 - 数据访问 - 高级教程 中。分布式

那么咱们真的不须要存储库这种东西吗?答案是否认的,至少在实践领域驱动设计的应用中。还记得在上一篇文章 如何运用领域驱动设计 - 聚合 中,咱们不止一次的提到了仓储这个概念,由于它是为聚合而服务的,而随着领域的深刻,使得领域模型愈来愈复杂的时候,存储库将慢慢变成模型的扩展,它将描述您每个用例检索聚合的意图。

思考一下,您现有的应用中是否包含了一个全能的ORM框架(好比EF),那您引入仓储的缘由是什么呢?

什么是存储库

好吧,此次的开篇太长了,终于回到了正题:什么是存储库? 原著《领域驱动设计:软件核心复杂性应对之道》 中对存储库的有关解释:

为每种须要全局访问的对象类型建立一个对象,这个对象就至关于该类型的全部对象在内存中的一个集合的“替身”。经过一个众所周知的接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或删除数据的操做。提供根据具体标准来挑选对象的方法,并返回属性值知足查询标准的对象或对象集合(所返回的对象是彻底实例化的),从而将实际的存储和查询技术封装起来。只为那些确实须要直接访问的Aggregate提供Repository。让客户始终聚焦于型,而将全部对象存储和访问操做交给Repository来完成。

国际惯例,让咱们来看看这一段话大体讲了什么。Repository提供了一个增删改查的操做,它抽象了数据访问的部分。是的,这个理解是很正确的,由于这是存储库很重要的特性。因此有不少同窗就开始疯狂的使用存储库了,在项目中大量的引入Repository,而嵌套于ORM之上。

可是!!!!! 咱们忽略了上面的其它几点:“确实须要直接访问的Aggregate提供Repository”“提供根据具体标准来挑选对象” 。 注意,这很重要,下文将一一为你们解释。

如何运用存储库

存储库是为聚合提供操做

这一点是很是关键的,存储库是为聚合而服务的。有关于聚合的部分,能够查看上一篇文章 如何运用领域驱动设计 - 聚合。为何呢它必定要为聚合服务? 它不能为实体服务吗? 由于聚合是一个总体,在上一文中咱们已经说过了,当凝练出一个聚合根的时候,就证实外界只能经过聚合根来访问聚合内的实体,因此咱们没有理由在任何一个地方须要穿透聚合根去访问实体,这是错误而且没有意义的。那么很天然的就能够衍生出:咱们何时须要使用存储库单独来提取实体呢?好像确实没有。不过有的同窗会说了,我在作**报表的时候,我就确实须要只访问某个实体呀?那么请思考两个点:一、该实体是否须要提高为聚合根。 二、若是是普遍查询的报表,可能并不须要经过仓储来获取对象,须要专门的查询框架来完成。

所以,咱们创建出来的仓储的接口多是这个样子的:

public interface IRepository<TAggregateRoot>
    where TAggregateRoot : class, IAggregateRoot
{
}

此处使用了C#的接口泛型约束,将仓储的服务者约束为了一个聚合根。该代码在上文介绍的 MiCake 中您也能够看到。

存储库对外提供哪些方法

到目前为止,咱们已经知道一个存储库至少应该包含根据ID来对聚合的增删改查方法,可能有一些时候咱们只须要查,不须要删。可是就一个通用的存储库来讲,它能具备这些方法是毫无疑问的。因此咱们的仓储接口能够增长一些通用方法:

public interface IRepository<TAggregateRoot>  
        where TAggregateRoot : class, IAggregateRoot
{
    TAggregateRoot Find(TKey Id);

    void Add(TAggregateRoot aggregateRoot);

    void Update(TAggregateRoot aggregateRoot);

    void Delete(TAggregateRoot aggregateRoot);
}

存储库是一个明确的约定

虽然存储库提供了基础的提取方法,可是在许多场景下,咱们可能更须要根据某种条件来从数据库中读取对应的模型并将其转换为领域聚合对象。好比在以前的一篇文章 如何运用领域驱动设计 - 领域服务 中就有一个地方出现了使用存储库的状况:咱们须要根据当前的位置来查找附近的饭店:

var nearbyRestaurants = restaurantRepository.GetNearbyRestaurant(currentAddress);

采用了相似于这样的写法。该存储库对外提供了一个GetNearbyRestaurant的方法出来,外界的应用服务就能够经过该方法来获取对应的结果。

这是一个很好的方法签名,咱们经过传入一个当前位置就可以获取到附近的饭店。经过阅读存储库提供出来的方法就能理解领域中的检索意图,从侧面也反应了领域的某些用例。

可是,如今有部分的同窗热爱另一种写法:经过Lambda做为方法参数,传递给下层的ORM框架来进行查询。该方法签名相似于这样:

IQueryable<TEntity> FindMatch(params Expression<Func<TEntity, object>>[] propertySelectors);

这样作的好处是全部的存储库均可以复用这个接口,之后全部的查询均可以经过使用该方来来完成,而不须要再去单独写各类Find方法。经过返回一个IQueryable对象,甚至能够将业务查询逻辑直接放到应用层,这样想怎么操做就怎么操做。

请注意!!!这很是的危险!!!! 您可能会问了:“我平时所接触的框架或者仓储不都是这样写的吗?能够实现我任何的业务查询,爽歪歪。” 可是这样写正在逐渐丧失存储库原有的做用。回到开篇提到的一个问题:假如使用了EF这样的ORM框架,为何还须要嵌套一层仓储呢? 而如今,您可能正在这样作,开放且灵活的约定,再加上延迟的IQueryable对象,让仓储层彻底丧失了原有的做用,它反而成了负担,为何不直接使用DbContext对象呢? 为了仓储而使用仓储,为了看上去像DDD而DDD,那不是本身骗本身吗?

因此请尽可能避免在您的存储库中去写这种灵活而没有任何明确检索意图的方法接口,它可能确实会使您减小代码书写量,但随着项目的复杂和领域对象的逐渐增多,它会使您的应用层愈来愈迷惑。因此存储库中所提供的应该是具备明确约定的方法

这里我摘抄了 领域驱动设计模式、原理与实践 中的一段话,我以为它的描述很是好:

存储库不是一个对象。它是一个程序边界以及一个明确的约定,在其上命名方法时它须要的工做量与领域模型中的对象所需的工做量同样多。你的存储库约定应该是特定的以及可以揭示意图并对领域专家具备意义。

具备领域意图的东西咱们都应该领域层,而相似于数据库的访问实现这类基础架构应该放在基础设施层。因此能够看出咱们抽象出来的仓储接口是应该放在领域层的,而仓储的实现能够放在基础设施层 。这个问题有不少小伙伴可能迷惑了好久,我上次看到一位同窗将仓储接口放在了应用层,由于它认为和领域无关,认为仓储只是一个提供增删改查的东西。而这也是由于忽略了仓储也是领域行为的一部分的结果。

审计追踪

在前面讲值对象的文章中,有一位园友问了我一个问题,有一点是:相似于CreateDate,CreateUser这种审计信息,咱们许多时候都会依附在领域对象身上,那么是否是应该经过领域服务来作处理呢?

其实否则,它们虽然对咱们有参考意义,其实并无在捕获领域需求时捕获出来。每每这类审计信息都是咱们按照以往的开发经验所提炼出来的,因此它们对领域对象的影响很小。那么咱们又很须要去操做它们,好比持久化一个聚合根的时候,为它附带上建立时间,这样便于咱们去追踪它的一些记录。而此时,就能够依赖咱们的存储库来完成了,当聚合根在领域服务或者领域用例中已经完成了操做时,将它传递给存储库持久化以前就可让存储库为它加上审计信息。

汇总

存储库有时还能够拥有对集合汇总的功能,好比上面咱们提到了饭店的一个仓储,可能咱们在系统中想获得我系统中到底有多少个饭店,或者在某个区域有多少个饭店。这种汇总的功能您也能够交给存储库来完成,这也完美的符合“存储库”中“库”的含义。但仍是请注意,这些汇总的方法依然得拥有一个明确的约定格式,不要由于是汇总就将存储库写的开放而过于灵活。

有时候您可能须要造成一个报表,该报表它包含了各个领域对象的汇总状况。在此时,该汇总的职责可能并不属于存储库了,它须要您使用另外的方式来完成,该内容能够看下面的小节。

不要使用过多特性干扰您的领域对象

在持久化的过程当中,如今的主流方式咱们都会依赖于相似于EF Core这样的ORM框架来完成。当咱们须要将领域对象转换为数据库的数据对象(能够理解为表吧)时,可能有时候就须要代表什么是主键,什么具备约束等状况。若是您正在使用EF Core,对于 Data annotations 您可能再熟悉不过了,它提供了经过特性来标记的写法完成映射关系:

public class CustomerWithoutNullableReferenceTypes
{
    public int Id { get; set; }
    [Required]            // Data annotations needed to configure as required
    public string FirstName { get; set; }
    [Required]
    public string LastName { get; set; }     // Data annotations needed to configure as required
    public string MiddleName { get; set; }   // Optional by convention
}

该代码摘自 EF Core 教程 - 必需和可选属性

这种写法很诱人,由于只须要简单的在属性上增长一个特性就完成了配置。可是!!!这些特性对领域对象实际上是没有必要的,它可能还会干扰您的阅读。由于咱们在构建领域对象的时候不该该考虑数据持久层面的问题,而构建出来的领域对象也应该保持干净。

在EFCore中,为咱们提供了Fluent API的方式来配置模型,该方式能够很好的让领域对象保持干净。假如您没有使用EFCore,另外的ORM框架也必定会为您提供相似于这样的配置方法。

不要为了显示而使用存储库

不少场景咱们可能须要提供一个丰富的界面,或者一个完整的报表。好比在一个界面上显示了某个聚合中的一个实体的信息,又或者在报表中提供了各个实体和值对象的汇总和特定信息。在这个状况下,仓储可能就显得有点隆重了,我必需要经过A、B、C……仓储获取全部聚合A,B,C,而后再来处理汇总信息。要么就是将存储库的规则打破,直接查询利用EF Core查询出IQueryable集合对象,而后一顿输出猛如虎来达到效果。

记住不要为了使用DDD而让您的开发变得复杂而不顺手,在这个时候咱们甚至能够不使用存储库,咱们能够利用另外的框架来直接查询数据库,也或者是使用ADO.NET运用原生Sql来达到查询的效果。还有一种方法是将查询单独划分为应用系统的一个分支,将修改(命令)单独划分为另一个分支来操做领域对象。这是DDD的另一种模式,可能您已经听过它的英文简写了:CQRS。该模式的内容会在后期的文章中为你们介绍,MiCake后期也会增长对CQRS的支持。

工做单元

在持久化的过程当中,咱们必须保证一个聚合的全部的部分一同保持成功,或者一个用例的多个聚合同时保存成功(在分布式中可能只能追求最终一致性)。因此咱们必须得保证存储库是有事务的,而事务的管理是由工做单元来提供的。这也是为何存储库每次都和工做单元这一律念一同出现。下面引用了微软AspNet中的一张图,方便您理解工做单元(UnitOfWork):

UnitOfWork

该图片选取自 微软 AspNet 教程 - 实现存储库和工做单元模式

本章附带了关于工做单元和仓储接口的演示代码,关于工做单元的部分会在下篇文章为你们介绍。

持久化中的困难

关于持久化的问题已是一个老生常谈的话题了,在一篇关于值对象的博文中就已经说明了这个问题。如何将领域对象如何经过ORM来持久化到数据库?在回答这个问题以前,咱们得先理解一下什么是领域模型和数据模型:领域模型是问题域的抽象,富含行为和语言;数据模式是一种包含指定时间领域模型状态的存储结构,ORM能够将特定的对象(C#的类)映射到数据模型。数据模型和领域模型无关,存储库的做用就是保持这两个模型的独立而且不让它们变得模糊不清。

也就是说咱们在设计领域模型时应该仅仅关心领域中的对象,千万不要让框架(好比ORM)来驱动你的设计。关于这一点给了我一点灵感:既然咱们只关心领域对象,那在持久化的时候能不能单独创建一个持久化对象专门供ORM去映射到数据库,而仓储负责了聚合建立和保存的过程,在这个过程当中让仓储自动去完成领域对象到持久化对象的转换就好了。关于这个实现方法,准备在下下一块儿番外系列中为你们介绍,可能MiCake也会默认支持该方法来完成领域对象的持久化任务。固然,由于是番外的系列,因此为了您不错过这一部分,能够点击博客园右上角的关注。( 好吧,我又把上面的话不要脸的又复制了一遍 (ง •_•)ง)

总结

本次咱们介绍了有关领域驱动设计中“存储库”的内容,咱们知道了什么是存储库,以及如何去使用一个存储库。因为存储库属于一个很基础的概念,因此在该章节中咱们没有使用旅行记帐的案例来为你们介绍。而更多的是但愿你们可以理解使用存储库的场景和规范,毕竟如今存储库模式是很经常使用的一个模式,若是只知其然而不知其因此然的去使用存储库模式,不只体验不到它的益处,反而会让代码变得愈来愈复杂。

最后,提早祝你们元旦快乐。 (o゚v゚)ノ
*

相关文章
相关标签/搜索