Repository 仓储,你的归宿究竟在哪?(一)-仓储的概念

简介:

写在前面

写这篇博文的灵感来自《如何开始DDD(完)》,很感谢young.han兄这几天的坚持,陆陆续续写了几篇有关于领域驱动设计的博文,让园中再次刮了一阵“DDD探讨风”,我现在不像前段时间那样“疯狂”了,写博文需要灵感,就像这篇一样。那篇博文除去其他的一些问题探讨,留给我印象最深的就是:领域服务中使用仓储,下面摘自文中我的一段评论:

  1. 领域服务中去调用仓储,这一点是我一直所纠结的地方,我现在做的项目是领域服务中是不参杂着仓储的,这个操作是在应用层中,比如:_userRepository.Add(user);
  2. 兄台接下来说的观点,首先要明确一点的是:仓储应不应该在领域服务中进行调用???我上次写了那篇文章,其实到最后也没讨论出个结果,反正我现在所做的是,领域服务不实现仓储调用。你可以结合测试驱动开发就知道没什么了,DDD+TDD,其实领域模型最好的业务体现是在哪?不是在领域模型,而是领域模型的单元测试,它是很好的描述这个业务用例,如果你的领域模型的单元测试出了问题,那就是领域模型出了问题,其实兄台可以试着写下你这个业务场景下的领域模型的单元测试,也就是一个业务用例的单元测试,看看会发生什么?还有就是应用层的伪代码。

文中Luminji兄这样回复我:“领域服务不用仓储,那我们怎么单元测试领域服务?仅此一点,就说明领域服务必用仓储。反之,倒是上层,如控制器这里不应该用仓储。”其实原本大家的焦点不应该放在仓储上面的,而应该放在领域驱动设计的核心-领域模型上,为此我还曾写了几篇关于领域模型设计的博文,但是一个完整的应用程序不只是包含领域模型,还有其他的东西需要进行探讨,虽然它不像领域模型那么重要,但同样必不可少。

Luminji兄的评论,让我意识到需要把领域驱动设计中的其他概念明确探讨下了,如果对一些概念模糊不清,或者不能很好的明确其职责,这样就很容易导致我们在领域驱动设计的过程中陷入一些困境,就像我之前所掉进的坑-《设计窘境:来自 Repository 的一丝线索,Domain Model 再重新设计》。

以下内容只是个人对仓储概念及其问题进行探讨,并非是结论总结,仅供各位仁兄参考。

《实现领域驱动设计》

在进行正文探讨之前,我先啰嗦几句。

《实现领域驱动设计》这本书,我在之前觉得没必要阅读,因为当时认为学习领域驱动设计,只要精读下 Eric Evans 的经典著作《领域驱动设计-软件核心复杂性应对之道》就可以了,但是DDD是需要进行实践的,Eric Evans 只是提出领域驱动设计这个概念,有关于其实现,书中并没有花很大的精力去讲解,而《实现领域驱动设计》这本书正是弥补了这一点。

这两本书的阅读顺序,当然是先阅读《领域驱动设计》,然后再阅读《实现领域驱动设计》,如果你是第一次读第一本书,它会颠覆你对软件设计的一些看法,然后让你不能自拔的“爱上它”,不知道你有没有,反正我是这样,然后你在做一些应用程序设计的时候,会尝试使用领域驱动设计,虽然有些步履蹒跚,但是走出第一步是很重要的。关于阅读第二本书,我的建议是,在阅读之前,先根据第一本书中的指导,自己尝试去实践领域驱动设计,最好是做一些实际业务场景的应用,在这个过程中,完全按照自己对领域驱动设计的想法去实现,虽然可能会掉进一些深坑,但是我觉得只有这样你才会理解的更加深刻。至于为什么自己实践过领域驱动设计再去阅读第二本书?因为实践过后阅读的话,你会与作者产生一些共鸣,这是很奇妙的感觉,就像译者-腾云这样所说:

《实现领域驱动设计》这本书,我现在也只读了第十二章-资源库(译者把 Repository 翻译为资源库,和仓储是一个意思,我更喜欢仓储这个名词,后面就用它来表示 Repository 了),阅读仓储这一章的时候,我是带着问题进行阅读的,也就是仓储的职责是什么?它的归宿究竟在哪?但是很可惜,我在这一章节中并没有找到我要寻找的答案,因为作者主要讲解的是仓储的实现,但是我发现了其他一些有意思的东西,下面希望和各位仁兄分享下(或许有点偏离主题了,但是我觉得应该会蛮有意义的)。

仓储(Repository) VS 数据访问对象(DAO)

有关于仓储的概念,我不止在一篇博文中进行说明,但是这边既然和数据访问对象进行比较的话,还是要声明一下,下面来自《领域驱动设计》书中的定义:

Repository(仓储):协调领域和数据映射层,利用类似与集合的接口来访问领域对象。

也可以像 dudu 这样进行直白的理解:Repository 是一个独立的层,介于领域层与数据映射层(数据访问层)之间。它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问。Repository 是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。

仓储是领域驱动设计中产生的概念,也就是说,如果你的应用程序不是基于领域驱动设计的,那在设计中使用仓储是不是有点不伦不类呢?首先,就像 Eric Evans 所定义中明确的那样:协调领域和数据映射层,两个关键字领域数据映射层,这里面的领域是指领域模型(实体和值对象),这是桥的一头,另一头就是数据映射层,也就是我们常说的 ORM 工具,在 .NET 领域也就是我们常用的 EntityFramework,很多人认为 EntityFramework 就包含仓储,好像之前有人发表过博文阐述过这个问题,但是你看下仓储的定义,就会发现这不是一个概念的问题。除了这两个关键词,还有一个动词就是协调,仓储协调的是什么?怎么协调的?这个概念需要明确下,桥的一头-领域模型(主要是实体对象),这个就不多说了,桥的另一头-ORM(对象关系映射),因为我们大部分情况下使用的是关系型数据库,如何对数据进行管理?当然 DAO 是一种(这边先不多说),还有就是使用 ORM,它可以让你很方便的进行数据和对象映射转换,如果你的项目是基于事务脚本模式设计的,那就没必要使用 ORM 工具了,因为使用简单的 SQL 更合适,说了这么多,好像都没说到重点,其实仓储协调的是 ORM 中的“O”,也就是对象的概念,它是在数据映射层之上的,是一种概念,而不是一种实现,这个概念很重要。

有时候,仓储和数据访问对象会当作同义词来看待,因为他们都提供了对持久化机制的抽象,在 DAO 中比较好理解,仓储中的持久化机制主要体现在 ORM 中,但是这并不属于仓储,更不属于 DAO,所以有时候我们认为所有的持久化抽象称为 DAO,并不是很准确,我们需要确定的是这种模式是否得到了真正的实现。

仓储和 DAO 是不同的,一个 DAO 主要从数据库表的角度来看待问题,并且提供 CRUD 操作,这种模式适用于事务脚本程序中,这是因为,这些与 DAO 相关的模式通常只是对数据库表的一层封装。而另一方面,仓储和数据影射器(ORM)则更加偏向于对象,因此通常被用于领域模型中。

还有一点内容就是存储过程的探讨,在《实现领域驱动设计》书中,作者也提到了,他不建议我们在基于领域驱动设计的应用中去使用存储过程,因为我们的建模团队并不能很好的理解存储过程所使用的语言,此外,通常来说他们也看不到存储过程的实现,而这些都是有饽于 DDD 目标的,但是有时候使用存储过程是为了程序性能,这是一个取舍的问题,就像我们使用 ORM 一样,我们需要对这个概念进行明确清楚,以防止我们在领域驱动设计的过程中参杂一些其他的东西。

有关仓储和数据访问对象的探讨,最后的结论是,通常来说,你可以将仓储当作 DAO 来看待,但是请注意一点,在设计仓储时,我们应该采用面向集合的方式,而不是面向数据访问的方式。这有助于你将自己的领域当作模型来看待,而不是 CRUD 操作。

以下几段话来自netfocus兄:

  1. 仓储是面向领域的,仓储定义的目的不是db驱动的,仓储管理的数据的最小粒度是聚合根,这两点和DAO有很大不同;
  2. 仓储用于实现聚合的生命周期,聚合创建后,如果不用了,会放回仓储,需要用时,再从仓储取出来(也就是唤醒聚合的意思);所以仓储就是聚合的温床。按照仓储的定义,它是一个集合,所以我们只会为仓储提供类似集合的接口,比如Add,Remove,Get这种操作;因为集合没有Save的说法,所以仓储上不需要有Save,更不会有Commit,也不会有Delete等概念。因为是集合,所以可以理解为一个无限大的内存空间,我们不关心集合是否太大,也不关心背后的持久化,这些不是DDD该思考的东西,我们可以用Dapper来实现,也可以用Mongo,也可以用EF。
  3. Save, Delete, Commit这些都是持久化的概念,最多在应用层表达。

关于仓储(Repository),你必须知道的几个概念。

1,仓储的两种设计方式:面向集合和面向持久化

面向集合和面向持久化,这两种类型的仓储设计方式,在《实现领域驱动设计》中有很详细的讲解,作者还附带了几个具体的实现,比如 Hibernate 实现、TopLink 实现等等,这个必须赞一个,感兴趣的朋友,可以进行阅读下。这面我简单说明下,这两种设计方式的不同之处,举个最直白的例子。

面向集合方式:

this.UserRepository.Add(user);

面向持久化方式:

this.UserRepository.Save(user);

可能很多朋友看到这,会不以为然,需要明确一点,在领域驱动设计中,不论是变量或是方法的命名规则都非常重要,因为其代码就是代表着一种通用语言,你要让人家可以看懂。在面向集合方式中,新对象的添加使用的是 Add,而在面向持久化方式中,不论是新对象的添加或是修改,都是使用的 Save,如果是基于 Unit Of Work(工作单元),会有 Commit。

2,不允许同一聚合实例多次添加到仓储中

关于这一点其实很多人都知道,因为聚合存在唯一性,仓储是管理它的集合,所以不可能在集合中存在多个同一聚合。另外在面向集合方式实现中,当从仓储中获取一个对象并对其进行修改时,我们并不需要“重新保存”该对象到仓储中,因为集合维护了对该对象的引用,而修改将直接作用在该对象上。

3,仓储实现方法返回类型建议为 void

我们在定义仓储接口的时候,一般会这样定义:

bool Add(TAggregateRoot aggregateRoot);

比如添加聚合实例的方法返回值为 bool 类型,但是有时候返回 true 并不一定代表着该聚合实例成功添加到仓储中了,因此,对于仓储来说,返回 void 可能会是更好的方式。那如何判断该聚合实例成功添加到仓储中了呢?我们一般会在仓储实现中进行异常捕获,这一点内容,在书中有讲解,我们可以自定义异常信息,友好的抛出一个异常。

4,对聚合实例的批量操作,最好不要使用 addAll() 和 removeAll() 方法

有时候我们在单个事务中,对多个聚合实例进行添加或删除的时候,为了方便,我们会使用 addAll() 和 removeAll() 方法,但是,我们使用这种方式,并不能对单个聚合实例操作进行监控,建议方式是循环调用 add() 和 remove() 方法。

5,聚合中删除聚合实例的正确表达是什么?

有时候,在应用程序设计中,对实例对象的生命周期管理就代表着其业务逻辑的体现,我们一般在设计中删除对象使用的是 delete,具体表现是从数据库中直接将数据删除掉,这是在事务脚本中的实现方式,在领域驱动设计中,其实是不存在对象删除这一说法的,正确的表达应该是,将聚合实例标记为失活的(disabled),不可用的(unusable),也就是说在仓储所涵盖的内容里面,最好不要出现 delete,至于数据库具体持久化中的 delete,这个就不在仓储的概念之中了。

6,仓储在各层中的位置存放

在书中,作者是这样表述的:我们将仓储接口定义放在了与聚合相同的包中(书中所有的示例都是用 java 实现的),而将仓储中的实现类放在了 impl 子包中,这种方式被大量的 java 项目所采用,然而,在协作上下文中,团队成员们,将实现类放在了基础设施层中。

这一点我是和作者持相同观点,比如下面的解决方案:

7,仓储中的级联删除所引出的问题

关于这个问题,其实我也不是很理解,下面引自作者的一段话(P375):

有人可能会依赖于ORM所提供的生命周期事件来完成对象的级联删除。我刻意地没有使用这种方式,因为我强烈反对由聚合来管理持久化,同时我强烈地提倡只使用资源库来处理持久化。当然,有关这两者的争论非常激烈,并且还在继续。因此,在选择时,你需要多方权衡。但是请记住,DDD专家是不会首先考虑使用聚合来管理持久化的。

根据我的猜测,大概是这样的意思,主要是仓储的持久化管理,一种是使用 ORM 攻击所提供的持久化机制,这种方式就使得仓储依赖于这些技术的实现,但是可以为我们在实现仓储的时候省去很多事,比如我们使用 EntityFramework,你会发现我们在实现仓储的时候,变得异常简单。还有一种方式就是作者提到的,建议让仓储自身去实现持久化机制,但是这种方式实现起来比较复杂,我也没具体的找到其实现方法,这边就不多说。

8,Unit Of Work(工作单元)的使用

只需要记住一点:当 Unit Of Work 中的 commit() 方法执行时,所有发生在对象上的修改都将提交到数据库中。

9,count() or size()?

我们有时候计算聚合实例的总数,一般会将实现方法命名为 count(),但是因为仓储应该尽可能的模拟一个集合,因此建议接口定义如下:

int Size();

命名规则是我们在软件开发过程中,最容易忽略的一点,可能在一般的开发过程中不注意会没事,但是在领域驱动设计中,就像之前所表述的那样,代码代表着一种语言,不光是自己能看懂,还要让需求人员可以看懂,至少可以从名字上知道其代表的意思,这一点很重要。

10,聚合根下的子聚合正确方式

有时,如果我们要获取聚合根下的某些子聚合,我们不用先从资源库中获取到聚合根,然后再从聚合根中获取这些子聚合,而是可以直接从资源库中返回。在有些情况下,这种做法是有好处的。比如,某个聚合根拥有一个很大的实体类型集合,而你需要根据某种查询条件返回该集合中的一部分实体。当然,只有在聚合根中提供了对该实体集合的导航时,我们才能这么做,否则,我们便违背了聚合的设计原则。我建议不要因为客户端的方便而提供这种访问方式。更多的时候,采用这种方式是由于性能上的考虑,比如从聚合根中访问子聚合将带来性能瓶颈的时候。此时的查找方法和其他查找方法具有相同的基本特征,只是它直接返回聚合根下的子聚合,而不是聚合根本身。无论如何,请慎重使用这种方式。

以上是书中作者的观点描述,其实最终也没有表述出一个正确的方式,只是说直接访问子聚合,作者不建议这样做,但是有时候为了一些性能问题,我们又不得不权衡利弊一下。除了这个问题之外,还有一个就是仓储执行完查询后,有时候会返回多个聚合的查询结果对象,这个我们一般会将查询结果放在一个值对象中。

11,CQRS 模式引入

对于 CQRS 模式,我没有深入研究过,更没有实践应用过,我的想法是先去把经典DDD理解透,然后再去尝试其他东西,毕竟路要一步一步走,CQRS 模式是对 DDD 的一种很好补充,也就是说它的产生是有一定的理由的,对于领域驱动设计初学者,我个人不建议,一开始就使用 CQRS 模式。

当我们使用用例优化查询时,有时候我们必须创建多个查询方法,什么意思?就是跨聚合查询,这可能意味着你的聚合边界划分的有问题,如果你确定你的聚合边界划分没有问题,那你应该考虑使用 CQRS 模式了,它的应用场景就是这样,凡事都有产生的原因,如果你的应用程序没有很复杂的查询操作,我个人觉得,完全没必要使用 CQRS 模式,有时候不要为了实现而实现。

12,共享仓储

对于这个概念,我没有深入研究过,作者也只是提出了一个思考,这边也不多说,思考如下:

为不同的聚合类型提供单独的资源库究竟给我们带来了什么好处?在聚合子类较少的情况下,为它们使用单独的资源库可能是最好的方式。但是,随着聚合子类数目的增加,而同时它们又具有完全的可互换性时,使用一个共享的资源库便更合适了。

写在最后

本来想一篇博文写完了事,但是看了下内容,写了还蛮多的,其实都还没说到重点上,只是大致讲述了仓储的概念,为防止大家看得累,那分为上下篇来进行讲解。

下篇主要对:仓储,你的归宿究竟在哪?这个问题进行探讨,内容主要包含其职责及调用场景的可行性探讨,具体用代码来验证。

这一篇内容就到这里,欢迎大家拍砖讨论。



本文转自田园里的蟋蟀博客园博客,原文链接:http://www.cnblogs.com/xishuai/p/ddd_repository.html,如需转载请自行联系原作者

相关文章
|
4月前
|
安全 Java 关系型数据库
JavaWeb仓储管理系统优化设计
JavaWeb仓储管理系统优化设计
34 0
|
5月前
|
存储 安全 数据库
淘东电商项目(17) -DTO接口细分
淘东电商项目(17) -DTO接口细分
38 0
|
6月前
|
供应链
企业考虑使用仓储管理系统的原因
仓储管理系统是帮助企业管理仓库及其库存,结合移动扫码应用,可以减少人员的人工操作、库存盘点和拣选所花费的时间,帮助高效管理仓库物料。
30 0
|
7月前
|
存储 Java 数据库
领域驱动设计(DDD)中的仓储,工厂,和领域层
领域驱动设计(DDD)中的仓储,工厂,和领域层
|
9月前
|
存储 人机交互 领域建模
领域模型随想
关于领域模型
83 0
|
存储 供应链 监控
仓储管理系统流程
仓储管理系统流程
580 0
仓储管理系统流程
|
SQL 存储 NoSQL
《领域驱动设计》:从领域视角深入仓储(Repository)的设计和实现
一、前言” DDD设计的目标是关注领域模型而并非技术来创建更好的软件,假设开发人员构建了一个SQL,并将它传递给基础设施层中的某个查询服务然后根据表数据的结构集取出所需信息,最后将这些信息提供给构造函数或者Factory,开发人员在做这一切的时候早已不把模型看做重点了,这个整个过程就变成了数据处理的风格 ”——Eric Evans《领域驱动设计》《领域驱动设计》(DDD)中的Repository(
《领域驱动设计》:从领域视角深入仓储(Repository)的设计和实现