内容提供者是存储库模式的一种实现吗?

40

仓储模式是由Hieatt和Rob Mee定义的设计模式,它使用类似集合的接口来访问领域对象,从而在领域层和数据映射层之间起到中介作用。

MSDN Repository Pattern

基本上,它将一个或多个I/O设备(云、磁盘、数据库等)抽象成一种常见的集合式接口,您可以在其中读取、写入、查找和删除数据

Fernando Cejas的Android Clean Architecture中,应用程序所需的所有数据都通过该层从存储库实现获取(接口位于域层),并使用存储库模式和策略来选择不同的数据源,具体取决于某些条件。

Content Provider

然而,正如Douglas Schmidt教授在Coursera课程中指出的那样,内容提供商管理和调节对一个或多个应用程序的数据中心仓库的访问

Content Provider

在书籍《Programming Android》中,内容提供者被用作RESTful Web服务的外观模式。这种方法最初是由Virgil Dobjanschi在2010年Google I/O大会上提出的
因此,与其将内容提供者用于访问本地SQLite数据库,为什么不将其作为存储库模式本身呢?

enter image description here

8个回答

27
让我们尝试比较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可能需要projectionwhere-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

8
简短的回答:ContentProvider是数据源而不是仓库。
SQL-Database/Android-Contentproviders/Repositories的目的是创建/读取/更新/删除/查找数据。
仓库通常操作于高级业务特定Java类(如Customer、Order、Product等),而SQL-Database和Android-Contentproviders操作于低级表格、行和列作为数据源。
因为SQL-Database不是仓库,所以Android-Contentprovider也不是仓库。
但是您可以通过使用基础Contentprovider来实现仓库。

6

我会提到来自Android框架团队的Dianne Hackborn,以表达我的观点。

ContentProvider

最后,ContentProvider是一个将数据从应用程序发布到其他地方的相当专业的工具。人们通常认为它们是数据库的抽象,因为有很多API和支持内置在其中以处理这种常见情况...但从系统设计的角度来看,这并不是它们的重点。

对于系统而言,这些东西是应用程序发布命名数据项的入口点,由URI方案标识。因此,一个应用程序可以决定如何将其包含的数据映射到URI名称空间,分发这些URI给其他实体,这些实体可以使用它们来访问数据。这允许系统在管理应用程序时执行一些特定操作:

• 分发URI不需要应用程序保持运行状态,因此这些URI可以随处传播,即使拥有该URI的应用程序已经停止运行。只有在某人告诉系统“嘿,为我提供此URI的数据”时,系统才需要确保拥有该数据的应用程序正在运行,以便它可以要求应用程序检索并返回数据。

• 这些URI还提供了一个重要的细粒度安全模型。例如,一个应用程序可以放置剪贴板上的图像的URI,但将其内容提供程序锁定,使没有人能够自由访问它。当另一个应用程序从剪贴板中提取该URI时,系统可以给它一个临时的“URI权限授予”,以允许它只能访问该URI后面的数据,而不能访问应用程序中的其他内容。

我们不关心的是:

实际上,如何实现ContentProvider背后的数据管理并不重要;如果您不需要在SQLite数据库中处理结构化数据,则不要使用SQLite。例如,FileProvider助手类是一种简单的方法,可以通过ContentProvider使您的应用程序中的原始文件可用。

此外,如果您不想为其他人使用发布数据,则根本无需使用ContentProvider。确实,由于ContentProvider周围构建了各种辅助工具,因此这可能是将数据放入SQLite数据库并将其用于填充UI元素(例如ListView)的简便方法。但是,如果所有这些东西都使您正在尝试完成的工作更加困难,请随意不使用它,并使用更合适的数据模型来代替。

完整的文本在这里: https://plus.google.com/+DianneHackborn/posts/FXCCYxepsDU

4
Dianne刚刚回复说,如果Content Provider是仓储模式的框架实现,那就不是有意为之。 :) 它是为了在数据包含的应用(例如联系人、媒体等)与想要访问该数据的其他应用程序之间提供一个由操作系统进行调节的抽象层。因此,它提供的关键功能是一种命名数据项的方法,允许操作系统通过内容URI确定谁拥有数据,以及与该命名数据交互的标准API,以及操作系统确定和控制谁可以访问哪些数据的各种(众多)设施。 - JP Ventura

5
赞赏这个问题,这是一个很好的观察 : )。在我看来,这不是一个肯定或否定的问题,因为大多数与设计模式相关的主题都是相当一般的。答案取决于你考虑的上下文是什么:
如果你有一个完全依赖于平台的应用程序,也就是说只考虑Android生态系统的上下文,那么 ContentProvider 是 Repository 模式的实现。这里的论点是,ContentProvider 的设计目的是解决与 Repository 模式旨在解决的一些相同的挑战:
- 它提供了对数据层的抽象,因此代码不一定依赖于存储环境 - 不是从任何地方直接访问数据。你可以把所有的 SQL 查询(或其他)放在一个地方。当我第一次作为一个新手实现 ContentProvider 时,我感到非常惊讶,我的代码看起来多么干净,我可以如何舒适地做出改变。 - 集中数据并在多个客户端(其他应用程序、搜索小部件等)之间共享数据,并提供数据安全机制 - 你绝对可以定义与数据相关的行为(一种方法是使用 ContentObserver) - 这是一个非常好的方式,可以迫使你从早期就以单元测试/自动化测试为重点组织你的代码
如果你将上述所有内容与 Repository 模式的原则并排放置,就会发现它们有一些严重的相似之处。虽然不是所有的相似之处都得到了满足,但核心思想是相同的。
现在,考虑一个在多个环境(如 Web、移动、PC)中工作的应用程序,要求完全改变。依赖 ContentProvider 作为设计模式是一个坏主意。这本身不一定是一个坏主意,但设计模式必须被实现,以便其他人能尽快理解你的代码。你看,即使在这里,每个人都建议使用 ContentProvider 的常见用途: 作为数据源,或者无论如何是平台相关的东西。因此,如果你在已知目的的组件上强制实现一个实现,事情可能变得相当不清晰。更好的方法是按照经典模式组织你的代码。
如果你的应用程序在你的 Android 设备上是孤立的,你可以把两个概念合并起来。如果你的应用程序在更大范围内使用,在多个平台上使用,最好按照经典方式组织你的代码。

恭喜你给出了一个很好的答案,Alexandru :) - Mihai Alexandru-Ionut

2
这是一个有趣的问题。我认为我的第一个答案将是不,Content Provider不是Repository Pattern的实现。
正如您所提到的,Repository Pattern旨在将业务逻辑(领域)与数据层分离。这种方法允许您为业务逻辑创建单元测试(因此领域不应完全依赖于Android)。使用Content Provider,您需要在领域中拥有某些Android对象。
您可以想象一种方式来隐藏接口后面的Content Provider逻辑,但是您将失去Content Provider允许您执行的许多好东西。
如果您对Android Architecture感兴趣,我建议您查看这个Github项目Android Clean Architecture。您将找到一种很好的方法来分离您的表示层、领域和数据层,并且通过使用Repository Pattern来完成领域和数据之间的通信。
希望这能有所帮助!

使用内容提供程序来实现业务案例,而不是依赖于显式声明的接口,可以通过内容解析器来实现。在我看来,使用内容提供程序作为门面提供了一个可观察/可迭代的集合,但绝对不如 RxJava Observable 友好。 - JP Ventura

2
在我看来,更好的做法是将ContentProvider视为数据源,尽管数据可以以多种方式存储(如SQLite数据库、文件等),以保持架构与Android框架之间的某种独立性。
谷歌仓库提供了一些架构示例。其中一个包含了一个带有内容提供程序和存储库的架构示例: googlesamples/android-architecture/todo-mvp-contentproviders 以下是一些摘录:
“您可以使用内容提供程序支持此示例未涵盖的其他功能,从而提供以下可能的好处:
- 允许您安全地与其他应用程序共享存储在您的应用程序中的数据。 - 为您的应用程序添加自定义搜索支持。 - 开发小部件以访问您的应用程序中的数据。”

architecture todo-mvp-contentproviders


1
使用ContentProviders作为存储库的问题在于,您需要将Android Framework作为模型的依赖项。使用存储库模式可以轻松地模拟、测试和替换实现。
正确的方法是将ContentProvider隐藏在接口下,并通过此接口让模型访问数据。这样,您的代码就与平台解耦了。
基本上,ContentProvider是要抽象的I/O源。

内容解析器会在这种方法中充当接口,您可以使用MockCursor和MockContentProvider来测试您的领域模块。 Translated text: 在这种方法中,内容解析器将作为接口行为,您可以使用MockCursor和MockContentProvider来测试您的领域模块。 - JP Ventura
与仅使用常规模拟相比,您需要使用大量其他库和依赖项。 - Eduardo Bonet

0

内容提供者是一个 Android 组件,如果将仓库概念与此组件混合使用,会产生阻塞依赖于您的应用程序,这样做会让人感到不舒服。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接