模式:REPOSITORY

REPOSITORY模式是领域驱动设计中的一个重要概念,它作为模型与数据访问之间的抽象层,专注于领域对象的管理和查询。本文详细介绍了REPOSITORY模式的查询、实现、在框架中的应用以及与FACTORY模式的关系,强调了保持领域模型为中心的重要性,同时探讨了如何为关系数据库设计对象,以实现模型与数据库的合理映射。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

 

模式:REPOSITORY

REPOSITORY的查询

客户代码可以忽略REPOSITORY的实现,但开发人员不能忽略

REPOSITORY的实现

在框架内工作

REPOSITORY与FACTORY的关系

为关系数据库设计对象


模式:REPOSITORY

我们可以通过对象之间的关联来找到对象。但当它处于生命周期的中间时,必须要有一个起点,以便从这个起点遍历到一个ENTITY或VALUE。

无论要用对象执行什么操作,都需要保持一个对它的引用。那么如何获得这个引用呢?一种方法是创建对象,因为创建操作将返回对新对象的引用。第二种方法是遍历关联。我们以一个已知对象作为起点,并向它请求一个关联的对象。这样的操作在任何面向对象的程序中都会大量用到,而且对象之间的这些链接使对象模型具有更强的表达能力。但我们必须首先获得作为起点的那个对象。

想到这种方法的人并不多(实现ENTITY和应用AGGREGATE),尝试它的人就更少了,因为人们将大部分对象存储在关系数据库中。这种存储技术使人们自然而然地使用第三种获取引用的方式——基于对象的属性,执行查询来找到对象;或者是找到对象的组成部分,然后重建它。

数据库搜索是全局可访问的,它使我们可以直接访问任何对象。由此,所有对象不需要相互联接起来,整个对象关系网就能够保持在可控的范围内。是提供遍历还是依靠搜索,这成为一个设计决策,需要在搜索的解耦与关联的内聚之间做出权衡。Customer对象应该保持该客户所有已订的Order吗?应该通过Customer ID字段在数据库中查找Order吗?恰当地结合搜索与关联将会得到易于理解的设计。

遗憾的是,开发人员一般不会过多地考虑这种精细的设计,因为他们满脑子都是需要用到的机制,以便很有技巧地利用它们来实现对象的存储、取回和最终删除。

现在,从技术的观点来看,检索已存储对象实际上属于创建对象的范畴,因为从数据库中检索出来的数据要被用来组装新的对象。实际上,由于需要经常编写这样的代码,我们对此形成了根深蒂固的观念。但从概念上讲,对象检索发生在ENTITY生命周期的中间。不能只是因为我们将Customer对象保存在数据库中,而后把它检索出来,这个Customer就代表了一个新客户。为了记住这个区别,我把使用已存储的数据创建实例的过程称为重建。

领域驱动设计的目标是通过关注领域模型(而不是技术)来创建更好的软件。假设开发人员构造了一个SQL查询,并将它传递给基础设施层中的某个查询服务,然后再根据得到的表行数据的结果集提取出所需信息,最后将这些信息传递给构造函数或FACTORY。开发人员执行这一连串操作的时候,早已不再把模型当作重点了。我们很自然地会把对象看作容器来放臵查询出来的数据,这样整个设计就转向了数据处理风格。虽然具体的技术细节有所不同,但问题仍然存在——客户处理的是技术,而不是模型概念。诸如METADATA MAPPING LAYER这样的基础设施可以提供很大帮助,利用它很容易将查询结果转换为对象,但开发人员考虑的仍然是技术机制,而不是领域。更糟的是,当客户代码直接使用数据库时,开发人员会试图绕过模型的功能(如AGGREGATE,甚至是对象封装),而直接获取和操作他们所需的数据。这将导致越来越多的领域规则被嵌入到查询代码中,或者干脆丢失了。虽然对象数据库消除了转换问题,但搜索机制还是很机械的,开发人员仍倾向于要什么就去拿什么。

客户需要一种有效的方式来获取对已存在的领域对象的引用。如果基础设施提供了这方面的便利,那么开发人员可能会增加很多可遍历的关联,这会使模型变得非常混乱。另一方面,开发人员可能使用查询从数据库中提取他们所需的数据,或是直接提取具体的对象,而不是通过AGGREGATE的根来得到这些对象。这样就导致领域逻辑进入查询和客户代码中,而ENTITY和
VALUE OBJECT则变成单纯的数据容器。采用大多数处理数据库访问的技术复杂性很快就会使客户代码变得混乱,这将导致开发人员简化领域层,最终使模型变得无关紧要。

根据到目前为止所讨论的设计原则,如果我们找到一种访问方法,它能够明确地将模型作为焦点,从而应用这些原则,那么我们就可以在某种程度上缩小对象访问问题的范围。初学者可以不必关心临时对象。临时对象(通常是VALUE OBJECT)只存在很短的时间,在客户操作中用到它们时才创建它们,用完就删除了。我们也不需要对那些很容易通过遍历来找到的持久对象进行查询访问。例如,地址可以通过Person对象获取。而且最重要的是,除了通过根来遍历查找对象这种方法以外,禁止用其他方法对AGGREGATE内部的任何对象进行访问。

持久化的VALUE OBJECT一般可以通过遍历某个ENTITY来找到,在这里ENTITY就是把对象封装在一起的AGGREGATE的根。事实上,对VALUE的全局搜索访问常常是没有意义的,因为通过属性找到VALUE OBJECT相当于用这些属性创建一个新实例。但也有例外情况。例如,当我在线规划旅行线路时,有时会先保存几个中意的行程,过后再回头从中选择一个来预订。这些行程就是VALUE(如果两个行程由相同的航班构成,那么我不会关心哪个是哪个),但它们已经与我的用户名关联到一起了,而且可以原封不动地将它们检索出来。另一个例子是“枚举”,在枚举中一个类型有一组严格限定的、预定义的可能值。但是,对VALUE OBJECT的全局访问比对ENTITY的全局访问更少见,如果确实需要在数据库中搜索一个已存在的VALUE,那么值得考虑一下,搜索结果可能实际上是一个ENTITY,只是尚未识别它的标识。

从上面的讨论显然可以看出,大多数对象都不应该通过全局搜索来访问。如果很容易就能从设计中看出那些确实需要全局搜索访问的对象,那该有多好!

现在可以更精确地将问题重新表述如下:

在所有持久化对象中,有一小部分必须通过基于对象属性的搜索来全局访问。当很难通过遍历方式来访问某些AGGREGATE根的时候,就需要使用这种访问方式。它们通常是ENTITY,有时是具有复杂内部结构的VALUE OBJECT,还可能是枚举VALUE。而其他对象则不宜使用这种访问方式, 因为这会混淆它们之间的重要区别。随意的数据库查询会破坏领域对象的封装和AGGREGATE。技术基础设施和数据库访问机制的暴露会增加客户的复杂度,并妨碍模型驱动的设计。

有大量的技术可以用来解决数据库访问的技术难题,例如,将SQL封装到QUERY OBJECT中,或利用METADATA MAPPING LAYER进行对象和表之间的转换。FACTORY可以帮助重建那些已存储的对象(本章后面将会讨论)。这些技术和很多其他技术有助于控制数据库访问的复杂度。

有得必有失,我们应该注意失去了什么。我们已经不再考虑领域模型中的概念。代码也不再表达业务,而是对数据库检索技术进行操纵。REPOSITORY是一个简单的概念框架,它可用来封装这些解决方案,并将我们的注意力重新拉回到模型上。

REPOSITORY将某种类型的所有对象表示为一个概念集合(通常是模拟的)。它的行为类似于集合(collection),只是具有更复杂的查询功能。在添加或删除相应类型的对象时,REPOSITORY的后台机制负责将对象添加到数据库中,或从数据库中删除对象。这个定义将一组紧密相关的职责集中在一起,这些职责提供了对AGGREGATE根的整个生命周期的全程访问。

客户使用查询方法向REPOSITORY请求对象,这些查询方法根据客户所指定的条件(通常是特定属性的值)来挑选对象。REPOSITORY检索被请求的对象,并封装数据库查询和元数据映射机制。REPOSITORY可以根据客户所要求的各种条件来挑选对象。它们也可以返回汇总信息,如有多少个实例满足查询条件。REPOSITORY甚至能返回汇总计算,如所有匹配对象的某个数值属性的总和,如下图所示。

REPOSITORY解除了客户的巨大负担,使客户只需与一个简单的、易于理解的接口进行对话,并根据模型向这个接口提出它的请求。要实现所有这些功能需要大量复杂的技术基础设施,但接口很简单,而且在概念层次上与领域模型紧密联系在一起。

因此:

为每种需要全局访问的对象类型创建一个对象,这个对象相当于该类型的所有对象在内存中的一个集合的“替身”。通过一个众所周知的全局接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或删除数据的操作。提供根据具体条件来挑选对象的方法,并返回属性值满足查询条件的对象或对象集合(所返回的对象是完全实例化的),从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的AGGREGATE根提供REPOSITORY。让客户始终聚焦于模型,而将所有对象的存储和访问操作交给REPOSITORY来完成。

REPOSITORY有很多优点,包括:

  • 它们为客户提供了一个简单的模型,可用来获取持久化对象并管理它们的生命周期;
  • 它们使应用程序和领域设计与持久化技术(多种数据库策略甚至是多个数据源)解耦;
  • 它们体现了有关对象访问的设计决策;
  • 可以很容易将它们替换为“哑实现”(dummy implementation),以便在测试中使用(通常使用内存中的集合)。

REPOSITORY的查询

所有REPOSITORY都为客户提供了根据某种条件来查询对象的方法,但如何设计这个接口却有很多选择。

最容易构建的REPOSITORY用硬编码的方式来实现一些具有特定参数的查询。这些查询可以形式各异,例如,通过标识来检索ENTITY(几乎所有REPOSITORY都提供了这种查询)、通过某个特定属性值或复杂的参数组合来请求一个对象集合、根据值域(如日期范围)来选择对象,甚至可以执行某些属于REPOSITORY一般职责范围内的计算(特别是利用那些底层数据库所支持的操作)。

尽管大多数查询都返回一个对象或对象集合,但返回某些类型的汇总计算也符合REPOSITORY的概念,如对象数目,或模型需要对某个数值属性进行求和统计。

在任何基础设施上,都可以构建硬编码式的查询,也不需要很大的投入,因为即使它们不这些事,有些客户也必须要做。

在一些需要执行大量查询的项目上,可以构建一个支持更灵活查询的REPOSITORY框架。如下图所示。这要求开发人员熟悉必要的技术,而且一个支持性的基础设施会提供巨大的帮助。

基于SPECIFICATION(规格)的查询是将REPOSITORY通用化的好办法。客户可以使用规格来描
述(也就是指定)它需要什么,而不必关心如何获得结果。在这个过程中,可以创建一个对象来
实际执行筛选操作。第9章将深入讨论这种模式。

基于SPECIFICATION的查询是一种优雅且灵活的查询方法。根据所用的基础设施的不同,它可能易于实现,也可能极为复杂。Rob Mee和Edward Hieatt在[Fowler 2002]一书中探讨了设计这样的REPOSITORY时所涉及的更多技术问题。 

即使一个REPOSITORY的设计采取了灵活的查询方式,也应该允许添加专门的硬编码查询。这些查询作为便捷的方法,可以封装常用查询或不返回对象(如返回的是选中对象的汇总计算)的查询。不支持这些特殊查询方式的框架有可能会扭曲领域设计,或是干脆被开发人员弃之不用。

客户代码可以忽略REPOSITORY的实现,但开发人员不能忽略

持久化技术的封装可以使得客户变得十分简单,并且使客户与REPOSITORY的实现之间完全解耦。但像一般的封装一样,开发人员必须知道在封装背后都发生了什么事情。在使用REPOSITORY时,不同的使用方式或工作方式可能会对性能产生极大的影响。

Kyle Brown曾告诉过我他的一段经历,有一次他被请去解决一个基于WebSphere的制造业应用程序的问题,当时这个程序正向生产环境部署。系统在运行几小时后会莫名其妙地耗尽内存。Kyle在检查代码后发现了原因:在某一时刻,系统需要将工厂中每件产品的信息汇总到一起。开发人员使用了一个名为all objects(所有对象)的查询来进行汇总,这个操作对每个对象进行实例化,然后选择他们所需的数据。这段代码的结果是一次性将整个数据库装入内存中!这个问题在测试中并未发现,原因是测试数据较少。

这是一个明显的禁忌,而一些更不容易注意到的疏忽可能会产生同样严重的问题。开发人员需要理解使用封装行为的隐含问题,但这并不意味着要熟悉实现的每个细节。设计良好的组件是有显著特征的(这是第10章的重点之一)。

正如第5章所讨论的那样,底层技术可能会限制我们的建模选择。例如,关系数据库可能对复合对象结构的深度有实际的限制。同样,开发人员要获得REPOSITORY的使用及其查询实现之间的双向反馈。

REPOSITORY的实现

根据所使用的持久化技术和基础设施不同,REPOSITORY的实现也将有很大的变化。理想的实现是向客户隐藏所有内部工作细节(尽管不向客户的开发人员隐藏这些细节),这样不管数据是存储在对象数据库中,还是存储在关系数据库中,或是简单地保持在内存中,客户代码都相同。REPOSITORY将会委托相应的基础设施服务来完成工作。将存储、检索和查询机制封装起来是REPOSITORY实现的最基本的特性,如下图所示。

REPOSITORY概念在很多情况下都适用。可能的实现方法有很多,这里只能列出如下一些需要谨记的注意事项。

  • 对类型进行抽象。REPOSITORY“含有”特定类型的所有实例,但这并不意味着每个类都需要有一个REPOSITORY。类型可以是一个层次结构中的抽象超类(例如,TradeOrder可以是BuyOrder或SellOrder)。类型可以是一个接口——接口的实现者并没有层次结构上的关联,也可以是一个具体类。记住,由于数据库技术缺乏这样的多态性质,因此我们将面临很多约束。
  • 充分利用与客户解耦的优点。我们可以很容易地更改REPOSITORY的实现,但如果客户直接调用底层机制,我们就很难修改其实现。也可以利用解耦来优化性能,因为这样就可以使用不同的查询技术,或在内存中缓存对象,可以随时自由地切换持久化策略。通过提供一个易于操纵的、内存中的(in-memory)哑实现,还能够方便客户代码和领域对象的测试。
  • 将事务的控制权留给客户。尽管REPOSITORY会执行数据库的插入和删除操作,但它通常不会提交事务。例如,保存数据后紧接着就提交似乎是很自然的事情,但想必只有客户才有上下文,从而能够正确地初始化和提交工作单元。如果REPOSITORY不插手事务控制,那么事务管理就会简单得多。

通常,项目团队会在基础设施层中添加框架,用来支持REPOSITORY的实现。REPOSITORY超类除了与较低层的基础设施组件进行协作以外,还可以实现一些基本查询,特别是要实现的灵活查询时。遗憾的是,对于类似Java这样的类型系统,这种方法会使返回的对象只能是Object类型,而让客户将它们转换为REPOSITORY含有的类型。当然,如果在Java中查询所返回的对象是集合时,客户不管怎样都要执行这样的转换。有关实现REPOSITORY的更多指导和一些支持性技术模式(如QUERY OBJECT)可以在[Fowler 2002]一书中找到。

在框架内工作

在实现REPOSITORY这样的构造之前,需要认真思考所使用的基础设施,特别是架构框架。这些框架可能提供了一些可用来轻松创建REPOSITORY的服务,但也可能会妨碍创建REPOSITORY的工作。我们可能会发现架构框架已经定义了一种用来获取持久化对象的等效模式,也有可能定义了一种与REPOSITORY完全不同的模式。

例如,你的项目可能会使用J2EE。看看这个框架与MODEL-DRIVEN DESIGN的模式之间有哪些概念上近似的地方(记住,实体bean与ENTITY不是一回事),你可能会把实体bean和AGGREGATE根当作一对类似的概念。在J2EE框架中,负责对这些对象进行访问的构造是EJB Home。但如果把EJB Home装饰成REPOSITORY的样子可能会导致其他问题。

一般来讲,在使用框架时要顺其自然。当框架无法切合时,要想办法在大方向上保持领域驱动设计的基本原理,而一些不符的细节则不必过分苛求。寻求领域驱动设计的概念与框架中的概念之间的相似性。这里的假设是除了使用指定框架之外没有别的选择。很多J2EE项目根本不使用实体bean。如果可以自由选择,那么应该选择与你所使用的设计风格相协调的框架或框架中的一些部分。

REPOSITORY与FACTORY的关系

FACTORY负责处理对象生命周期的开始,而REPOSITORY帮助管理生命周期的中间和结束。当对象驻留在内存中或存储在对象数据库中时,这是很好理解的。但通常至少有一部分对象存储在关系数据库、文件或其他非面向对象的系统中。在这些情况下,检索出来的数据必须被重建为对象形式。

由于在这种情况下REPOSITORY基于数据来创建对象,因此很多人认为REPOSITORY就是FACTORY,而从技术角度来看的确如此。但我们最好还是从模型的角度来看待这一问题,前面讲过,重建一个已存储的对象并不是创建一个新的概念对象。从领域驱动设计的角度来看,FACTORY和REPOSITORY具有完全不同的职责。FACTORY负责制造新对象,而REPOSITORY负责查找已有对象。REPOSITORY应该让客户感觉到那些对象就好像驻留在内存中一样。对象可能必须被重建(的确,可能会创建一个新实例),但它是同一个概念对象,仍旧处于生命周期的中间。

REPOSITORY也可以委托FACTORY来创建一个对象,这种方法(虽然实际很少这样做,但在理论上是可行的)可用于从头开始创建对象,此时就没有必要区分这两种看问题的角度了,如下图所示。

这种职责上的明确区分还有助于FACTORY摆脱所有持久化职责。FACTORY的工作是用数据来实例化一个可能很复杂的对象。如果产品是一个新对象,那么客户将知道在创建完成之后应该把它添加到REPOSITORY中,由REPOSITORY来封装对象在数据库中的存储,如下图所示。

另一种情况促使人们将FACTORY和REPOSITORY结合起来使用,这就是想要实现一种“查找或创建”功能,即客户描述它所需的对象,如果找不到这样的对象,则为客户新创建一个。我们最好不要追求这种功能,它不会带来多少方便。当将ENTITY和VALUE OBJECT区分开时,很多看上去有用的功能就不复存在了。需要VALUE OBJECT的客户可以直接请求FACTORY来创建一个。通常,在领域中将新对象和原有对象区分开是很重要的,而将它们组合在一起的框架实际上只会使局面变得混乱。

为关系数据库设计对象

在以面向对象技术为主的软件系统中,最常用的非对象组件就是关系数据库。这种现状产生了混合使用范式的常见问题(参见第5章)。但与大部分其他组件相比,数据库与对象模型的关系要紧密得多。数据库不仅仅与对象进行交互,而且它还把构成对象的数据存储为持久化形式。已经有大量的文献对于如何将对象映射到关系表以及如何有效存储和检索它们这样的技术挑战进行了讨论。最近的一篇讨论可参见[Fowler 2002]一书。有一些相当完善的工具可用来创建和管理它们之间的映射。除了技术上的难点以外,这种不匹配可能对对象模型产生很大的影响。

有3种常见情况:

  1. 数据库是对象的主要存储库;
  2. 数据库是为另一个系统设计的;
  3. 数据库是为这个系统设计的,但它的任务不是用于存储对象。

如果数据库模式(database schema)是专门为对象存储而设计的,那么接受模型的一些限制是值得的,这样可以让映射变得简单一点。如果在数据库模式设计上没有其他的要求,那么可以精心设计数据库结构,以便使得在更新数据时能更安全地保证聚合的完整性,并使数据更新变得更加高效。从技术上来看,关系表的设计不必反映出领域模型。映射工具已经非常完善了,足以消除二者之间的巨大差别。问题在于多个重叠的模型过于复杂了。MODEL-DRIVEN DESIGN的很多关于避免将分析和设计模型分开的观点,也同样适用于这种不匹配问题。这确实会牺牲一些对象模型的丰富性,而且有时必须在数据库设计中做出一些折中(如有些地方不能规范化)。但如果不做这些牺牲就会冒另一种风险,那就是模型与实现之间失去了紧密的耦合。这种方法并不要必须使用一种简单的、一个对象/一个表的映射。依靠映射工具的功能,可以实现一些聚合或对象的组合。但至关重要的是:映射要保持透明,并易于理解——能够通过审查代码或阅读映射工具中的条目就搞明白。

  • 当数据库被视作对象存储时,数据模型与对象模型的差别不应太大(不管映射工具有多么强大的功能)。可以牺牲一些对象关系的丰富性,以保证它与关系模型的紧密关联。如果有助于简化对象映射的话,不妨牺牲某些正式的关系标准(如规范化)。
  • 对象系统外部的过程不应该访问这样的对象存储。它们可能会破坏对象必须满足的固定规则。此外,它们的访问将会锁定数据模型,这样使得在重构对象时很难修改模型。

另一方面,很多情况下数据是来自遗留系统或外部系统的,而这些系统从来没打算被用作对象的存储。在这种情况下,同一个系统中就会有两个领域模型共存。第14章将深入讨论这个问题。或许与另一个系统中隐含的模型保持一致有一定的道理,也可能更好的方法是使这两个模型完全不同。

允许例外情况的另一个原因是性能。为了解决执行速度的问题,有时可能需要对设计做出一些非常规的修改。

但大多数情况下关系数据库是面向对象领域中的持久化存储形式,因此简单的对应关系才是最好的。表中的一行应该包含一个对象,也可能还包含AGGREGATE中的一些附属项。表中的外键应该转换为对另一个ENTITY对象的引用。有时我们不得不违背这种简单的对应关系,但不应该由此就全盘放弃简单映射的原则。

UBIQUITOUS LANGUAGE可能有助于将对象和关系组件联系起来,使之成为单一的模型。对象中的元素的名称和关联应该严格地对应于关系表中相应的项。尽管有些功能强大的映射工具使这看上去有些多此一举,但关系中的微小差别可能引发很多混乱。

对象世界中越来越盛行的重构实际上并没有对关系数据库设计造成多大的影响。此外,一些严重的数据迁移问题也使人们不愿意对数据库进行频繁的修改。这可能会阻碍对象模型的重构,但如果对象模型和数据库模型开始背离,那么很快就会失去透明性。

最后,有些原因使我们不得不使用与对象模型完全不同的数据库模式,即使数据库是专门为我们的系统创建的。数据库也有可能被其他一些不对对象进行实例化的软件使用。即使当对象的行为快速变化或演变的时候,数据库可能并不需要修改。让模型与数据库之间保持松散的关联是很有吸引力的。但这种结果往往是无意为之,原因是团队没有保持数据库与模型之间的同步。如果有意将两个模型分开,那么它可能会产生更整洁的数据库模式,而不是一个为了与早前的对象模型保持一致而到处都是折中处理的拙劣的数据库模式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr___Ray

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值