让我们尝试比较Martin Fowler(与Dave Rice、Matthew Foemmel、Edward Hieatt、Robert Mee和Randy Stafford)的《企业应用架构模式》一书中对存储库模式的定义与我们对ContentProviders的了解。
该书指出:存储库通过使用类似于集合的接口来访问域对象,从而在域和数据映射层之间进行介质操作。
重要的部分是"访问域对象"。因此乍一看,存储库模式似乎仅适用于访问(查询)数据。然而,使用ContentProvider不仅可以访问(读取)数据,还可以插入、更新或删除数据。
然而,该书指出:对象可以像简单对象集合一样添加到存储库中并从其中删除,并且由存储库封装的映射代码将在幕后执行适当的操作。
所以,是的,存储库和内容提供程序似乎提供相同的操作(非常高级别的观点),尽管该书明确指出“对象的简单集合”,但这并不适用于ContentProvider,因为它需要来自客户端(使用特定ContentProvider的客户端)的Android特定ContentValues和Cursor进行交互。
此外,该书提到了“域对象”和“数据映射层”:
存储库在域和数据映射层之间进行调解
在底层,存储库将元数据映射与查询对象结合起来。元数据映射在元数据中保存了对象关系映射的详细信息。
元数据映射基本上意味着如何将SQL列映射到Java类字段。
正如已经提到的,ContentProvider从一个query()操作返回一个Cursor对象。在我看来,Cursor不是一个领域对象。此外,从游标到领域对象的映射必须由客户端(使用ContentProvider的人)完成。因此,在我的观点中,ContentProvider完全缺少数据映射。此外,客户端可能还需要使用ContentResolver来获取领域对象(数据)。在我看来,这个API与书中的定义存在明显的矛盾:
“Repository也支持实现领域和数据映射层之间的清晰分离和单向依赖。”
接下来,让我们关注仓储模式的核心思想:
“在一个拥有许多领域对象类型和许多可能查询的大型系统中,仓库减少了处理所有查询所需的代码量。仓库促进了规范模式(在这里的条件对象形式中),以纯面向对象的方式封装要执行的查询。因此,可以删除特定情况下设置查询对象的所有代码。客户端永远不需要考虑SQL,只能纯粹地按对象编写代码。”
ContentProvider需要一个URI(字符串)。因此,它并不是真正的“面向对象”的方式。此外,ContentProvider可能需要projection
和where-clause
。
因此,可以认为URI字符串是一种封装,因为客户端可以使用该字符串而不是编写特定的SQL代码,例如:
使用存储库,客户端代码构建标准,然后将其传递给存储库,并要求它选择与之匹配的对象。从客户端代码的角度来看,没有查询“执行”的概念;相反,通过满足查询规范来选择适当的对象。
使用URI(字符串)的ContentProvider似乎并不与该定义相矛盾,但仍然缺少强调的面向对象方式。此外,字符串不是可重用的标准对象,不能以一般方式重复使用它们来组成标准规范,以“减少处理所有查询所需的代码量”。
例如,要通过名称查找人员对象,我们首先创建一个条件对象,像这样设置每个单独的条件:criteria.equals(Person.LAST_NAME, "Fowler")和criteria.like(Person.FIRST_NAME, "M")。然后,我们调用repository.matching(criteria)返回一个表示姓氏为Fowler并且名字以M开头的人的域对象列表。
正如您已经在问题中提到的那样,Repository对于隐藏不同的数据源作为客户端不知道的实现细节也很有用。这对于ContentProviders来说也是正确的,并在书中指定:
Repository的对象源可能根本不是关系型数据库,这很好,因为Repository通过专门的策略对象很容易替换数据映射组件。出于这个原因,在具有多个数据库模式或域对象来源的系统以及在测试期间,当需要速度时仅使用内存对象是可取的。
由于仓库接口屏蔽了领域层对数据源的感知,因此我们可以在不改变客户端调用的情况下重构仓库内部的查询代码实现。实际上,领域代码无需关心领域对象的来源或去向。
因此,总结一下:Martin Fowler等人的书中的一些定义与ContentProvider的API匹配(如果忽略该书强调的面向对象的事实):
- 隐藏了存储库/ContentProvider具有不同数据源的事实
- 客户端永远不必在特定于数据源的DSL(如SQL)中编写查询。如果我们将URI视为非数据源特定,则对ContentProvider也是如此。
- 无论是Repository还是ContentProvider,都具有相同的“高级”数据操作集:读取、插入、更新和删除数据(如果忽略Fowler所说的关于面向对象和对象集合的内容,而ContentProvider使用Cursor和ContentValues)
然而,ContentProvider确实缺少书中描述的存储库模式的一些关键点:
- 由于ContentProvider使用URI(where子句也是字符串),客户端无法重用Matching Criteria对象。这是需要注意的重要事项。该书清楚地表示,存储库模式在“具有许多域对象类型和许多可能查询的大型系统中,可以减少处理所有查询所需的代码量”。不幸的是,ContentProvider没有Criteria对象,例如
criteria.equals(Person.LAST_NAME, "Fowler")
,可以被重用和用于组合匹配条件(因为必须使用字符串)。
- ContentProvider完全错过了数据映射,因为它返回一个
Cursor
。这非常糟糕,因为客户端(使用ContentProvider访问数据的客户端)必须将Cursor映射到域对象。此外,这意味着客户端了解存储库内部,例如列名称。 “存储库可以成为改善广泛使用查询的代码的可读性和清晰度的良好机制。”对于ContentProviders来说,这肯定不是真的。
因此,ContentProvider不是《企业应用架构模式》中定义的存储库模式的实现,因为它至少缺少我上面指出的两个基本要素。
请注意,正如书名所示,仓储模式适用于需要进行大量查询的企业应用程序。
Android开发人员倾向于使用“仓储模式”这个术语,但实际上并不是指Fowler等人描述的“原始”模式(用于查询的标准的高可重用性),而是指隐藏底层数据源(SQL、云等)和域对象映射的接口。
更多信息请参见:
http://hannesdorfmann.com/android/evolution-of-the-repository-pattern