Android中的存储库反模式

本文的翻译是在“ Android开发人员”课程开始时准备的专业“








官方Android应用程序体系结构指南建议使用Repository类来“提供干净的API,以便应用程序的其余部分可以轻松检索数据”。但是,我认为,如果在项目中使用此模式,则一定会陷入混乱的意大利面条式代码。



在本文中,我将向您介绍“存储库模式”,并说明为什么它实际上是Android应用程序的反模式。



资料库



前面提到的《应用程序体系结构指南》建议采用以下结构来组织表示层逻辑:







存储库对象在此结构中的作用如下:



存储库模块处理数据操作。它们提供了干净的API,因此应用程序的其余部分可以轻松检索此数据。他们知道从何处获取数据以及在更新数据时要调用什么API。您可以将存储库视为不同数据源(例如持久性模型,Web服务和缓存)之间的中介。



基本上,该指南建议使用存储库来抽象应用程序中的数据源。听起来很合理,甚至很有用,不是吗?



但是,我们不要忘记聊天不是扔袋子(在这种情况下,是写代码),而是使用UML图揭示体系结构主题-甚至更多。任何体系结构模式的真正测试都是在代码中实现,然后确定其优缺点。因此,让我们找到一些不太抽象的内容进行审查。



Android体系结构蓝图v2中的存储库



大约两年前,我回顾了Android体系结构蓝图的“第一版”。从理论上讲,他们应该实现一个干净的MVP示例,但是在实践中,这些蓝图导致了相当脏的代码库。它们确实包含名为View和Presenter的接口,但没有设置任何体系结构边界,因此它本质上不是MVP。您可以在此处查看给定的代码审查



从那时起,Google使用Kotlin,ViewModel和其他“现代”实践(包括存储库)更新了架构蓝图。这些更新的蓝图以v2为前缀。



让我们看一下v2蓝图中的TasksRepository接口



interface TasksRepository {
   fun observeTasks(): LiveData<Result<List<Task>>>
   suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
   suspend fun refreshTasks()
   fun observeTask(taskId: String): LiveData<Result<Task>>
   suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
   suspend fun refreshTask(taskId: String)
   suspend fun saveTask(task: Task)
   suspend fun completeTask(task: Task)
   suspend fun completeTask(taskId: String)
   suspend fun activateTask(task: Task)
   suspend fun activateTask(taskId: String)
   suspend fun clearCompletedTasks()
   suspend fun deleteAllTasks()
   suspend fun deleteTask(taskId: String)
}


即使在阅读代码之前,您也要注意此接口的大小-这已经是一个唤醒电话。即使在大型Android项目中,在一个界面中使用如此众多的方法也会引发问题,但是我们正在谈论的是只有2000行代码的ToDo应用。为什么这个琐碎的应用程序需要一个具有如此庞大的API表面的类?



存储库是上帝的对象



上一部分中问题的答案包含在TasksRepository方法的名称中。我可以将该接口的方法大致分为三个不重叠的组。



第一组:



fun observeTasks(): LiveData<Result<List<Task>>>
   fun observeTask(taskId: String): LiveData<Result<Task>>


第2组:



   suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
   suspend fun refreshTasks()
   suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
   suspend fun refreshTask(taskId: String)
   suspend fun saveTask(task: Task)
   suspend fun deleteAllTasks()
   suspend fun deleteTask(taskId: String)


第三组:



  suspend fun completeTask(task: Task)
   suspend fun completeTask(taskId: String)
   suspend fun clearCompletedTasks()
   suspend fun activateTask(task: Task)
   suspend fun activateTask(taskId: String)


现在,让我们定义以上每个组的职责范围。



第1组基本上是使用LiveData工具的Observer模式的实现。组2是数据存储的网关,另外还有两种方法refresh,这是必需的,因为远程数据存储隐藏在存储库后面。第3组包含的功能方法基本上实现了应用程序域逻辑的两个部分(任务完成和激活)。



因此,该界面具有三个不同的职责。难怪它是如此之大。并且尽管可以争辩说第一组和第二组作为单个界面的一部分存在是可以接受的,但添加第三组是不合理的。如果该项目需要进一步开发,并成为一个真正的Android应用程序,那么第三组将与该项目中域流的数量成正比。嗯



我们对担负这么多职责的类有一个特殊的术语:神的对象。这是Android应用程序中广泛使用的反模式。在这种情况下,Activitie和Fragment是标准的可疑对象,但是其他类也可以退化为Divine对象。特别是如果他们的名字以“经理”结尾,对吗?



等等...我想我为TasksRepository找到了一个更好的名字:



interface TasksManager {
   fun observeTasks(): LiveData<Result<List<Task>>>
   suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
   suspend fun refreshTasks()
   fun observeTask(taskId: String): LiveData<Result<Task>>
   suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
   suspend fun refreshTask(taskId: String)
   suspend fun saveTask(task: Task)
   suspend fun completeTask(task: Task)
   suspend fun completeTask(taskId: String)
   suspend fun activateTask(task: Task)
   suspend fun activateTask(taskId: String)
   suspend fun clearCompletedTasks()
   suspend fun deleteAllTasks()
   suspend fun deleteTask(taskId: String)
}


现在,此接口的名称更好地反映了它的职责!



贫血库



在这里您可能会问:“如果我将域逻辑从存储库中拉出来,是否可以解决问题?” 好了,回到Google手册中的“架构图”。



例如,如果您completeTask想从TasksRepository中提取方法,您将它们放在哪里?根据Google推荐的“架构”,您将需要将此逻辑移至您的ViewModels中。这似乎不是一个错误的决定,但确实如此。



例如,假设您将这种逻辑放入一个ViewModel中。然后,一个月后,您的客户经理希望允许用户从多个屏幕上完成任务(这与我曾经使用过的所有ToDo经理都有关)。 ViewModel内部的逻辑不能重复使用,因此您需要复制它或将其返回给TasksRepository。显然,这两种方法都是不好的。



更好的方法是将域流提取到自定义对象中,然后将其放在ViewModel和存储库之间。然后,不同的ViewModels将能够重用该对象来执行该特定线程。这些对象称为“用例”或“交互”...但是,如果将用例添加到代码库中,则存储库本质上将成为无用的模板。无论他们做什么,都将更适合用例。 Gabor Varadi已经在本文中讨论了这个主题,因此我将不做详细介绍。我几乎同意他所说的有关“贫血库”的所有内容。



但是,为什么用例比存储库好得多?答案很简单:用例封装了单独的流。因此,您将拥有几个高度针对性的用例类,而不是一个逐渐扩展为Divine对象的存储库(针对每个域概念)。如果流取决于网络和要存储的数据,则可以将适当的抽象传递给用例类,它将在这些源之间“仲裁”。



通常,防止存储库降级为Divine类同时避免不必要的抽象的唯一方法似乎是摆脱存储库。



Android以外的存储库。



现在,您可能想知道存储库是否是Google的发明。不,他们不是。在Google决定在其架构指南中使用存储库模式之前,就已经对其进行了描述。



例如,Martin Fowler在他的《企业应用程序体系结构模式》一书中描述了存储库。他的博客也有一篇客串文章描述了相同的概念。根据Fowler的说法,存储库只是存储层的包装,它提供了更高级别的查询接口以及可能的内存中缓存。我要说的是,从Fowler的角度来看,存储库的行为类似于ORM。



埃里克·埃文斯(Eric Evans)在他的《域驱动设计》一书中也描述了存储库。他写了:



, , , — . , . , , .


请注意,您可以将上面引用中的“存储库”替换为“ Room ORM”,这仍然有意义。因此,在域驱动设计的上下文中,存储库是一个ORM(手动或使用第三方框架实现)。



如您所见,该存储库不是在Android世界中发明的。这是所有ORM框架都基于的非常合理的设计模式。但是请注意,什么不是存储库:“经典”都没有争论过存储库应该尝试抽象化网络访问和数据库访问之间的区别。



实际上,我很确定他们会发现这个想法幼稚而自欺欺人。要了解原因,您可以阅读另一篇文章,这次是Joel Spolsky(StackOverflow的创始人)题为“泄漏抽象定律简而言之:网络与从数据库访问到抽象之间的差异太大,而没有明显的泄漏。



仓库如何在Android中变成反模式



那么Google是否误解了存储库模式,并引入了抽象网络访问的天真想法?我对此表示怀疑。



我在GitHub存储库中找到了指向该反模式的最早链接,不幸的是这是一个非常受欢迎的资源。我不知道这个特定的作者是否发明了这种反模式,但是看起来正是这个仓库在Android生态系统中普及了这个总体思路。Google开发人员可能是从那里或从第二个来源之一获得的。



结论



因此,Android中的存储库已成为一种反模式。它在纸上看起来不错,但即使在琐碎的应用程序中也有问题,并可能在大型项目中导致实际问题。



例如,在另一个Google蓝图中,这次是建筑组件,使用存储库最终导致了诸如NetworkBoundResource之类的瑰宝请记住,示例浏览器GitHub仍然是一个很小的〜2 KLOC应用程序。



据我所知,官方文档中定义的“存储库模式”与干净且可维护的代码不兼容。



感谢您的阅读,您可以像往常一样在下面留下您的评论和问题。






All Articles