Android开发中“真正”的仓库模式

  • 原文地址:https://proandroiddev.com/the-real-repository-pattern-in-android-efba8662b754
  • 原文发表日期:2019.9.5
  • 作者:Denis Brandi
  • 翻译:tommwq
  • 翻译日期:2024.1.3

Figure 1: 仓库模式

多年来我见过很多仓库模式的实现,我想其中大部分是错误而无益的。

下面是我所见最多的5个错误(一些甚至出现在Android官方文档中):

  1. 仓库返回DTO而非领域模型。
  2. 数据源(如ApiService、Dao等)使用同一个DTO。
  3. 每个端点集合使用一个仓库,而非每个实体(或DDD聚合根)使用一个仓库。
  4. 仓库缓存所有的域,即使是频繁更新的域。
  5. 数据源被多个仓库共享使用。

那么要如何把仓库模式做对呢?

1. 你需要领域模型

这是仓库模式的关键,我想开发者难以正确实现仓库模式的原因在于他们不理解领域是什么。

引用Martin Fowler的话,领域模型是:

领域中同时包含行为和数据的对象模型。

领域模型基本上表示企业范围内的业务规则。

对于不熟悉领域驱动设计构建块或分层架构(六边形架构,洋葱架构,干净架构等)的人来说,有三种领域模型:

  1. 实体:实体是具有标识(ID)的简单对象,通常是可变的。
  2. 值对象:没有标识的不可变对象。
  3. 聚合根(仅限DDD):与其他实体绑定在一起的实体(通常是一组关联对象的聚合)。

对于简单领域,这些模型看起来与数据库和网络模型(DTO)很像,不过它们也有很多差异:

  • 领域模型包含数据和过程,其结构最适于应用程序。
  • DTO是表示JSON/XML格式请求/应答或数据库表的对象模型,其结构最适于远程通信。

Listing 1: 领域模型示例

// Entity
data class Product( val id: String,val name: String,val price: Price,val isFavourite: Boolean
) {// Value objectdata class Price( val nowPrice: Double,val wasPrice: Double) {companion object {val EMPTY = Price(0.0, 0.0)}}
}

Listing 2: 网络DTO示例

// Network DTO
data class NetworkProduct(@SerializedName("id")val id: String?,@SerializedName("name")val name: String?,@SerializedName("nowPrice")val nowPrice: Double?,@SerializedName("wasPrice")val wasPrice: Double?
)

Listing 3: 数据库DTO示例

// Database DTO
@Entity(tableName = "Product")
data class DBProduct(@PrimaryKey                @ColumnInfo(name = "id")                val id: String,                @ColumnInfo(name = "name")                val name: String,@ColumnInfo(name = "nowPrice")val nowPrice: Double,@ColumnInfo(name = "wasPrice")val wasPrice: Double
)

如你所见,领域模型不依赖框架,对象字段提倡使用多值属性(正如你看到的Price逻辑分组),并使用空对象模式(域不可为空)。而DTO则与框架(Gson、Room)耦合。

幸好有这样的隔离:

  • 应用程序的开发变得更容易,因为不需要检查空值,多值属性也减少了字段数量。
  • 数据源变更不会影响高层策略。
  • 避免了“上帝模型”,带来更多的关注点分离。
  • 糟糕的后端接口不会影响高层策略(想象一下,如果你需要执行两个网络请求,因为后端无法在一个接口中提供所有信息。你会让这个问题影响你的整个代码库吗?)

2. 你需要数据转换器

这是将DTO转换成领域模型,以及进行反向转换的地方。

多数开发者认为这种转换是无趣又无效的,他们喜欢将整个代码库,从数据源到界面,与DTO耦合。

这也许能让第一个版本更快交付,但不在表示层中隐藏业务规则和用例,而是省略领域层并将界面与数据源耦合会产生一些只会在生产环境遇到的故障(比如后端没有发送空字符串,而是发送null,并因此引发NPE)。

以我所见,转换器写起来快,测起来也简单。即使实现过程缺乏趣味,它能保护我们不会因数据源行为的改变而受到意外影响。

如果你没有时间(或者干脆懒得)进行数据转换,你可以使用对象转换框架,比如ModelMapper - Simple, Intelligent, Object Mapping. 来加快进度。

我不喜欢在代码中使用框架,为减少样板代码,我建立了一个泛型转换接口,以免为每个转换器建立独立接口:

interface Mapper<I, O> {fun map(input: I): O
}

以及一组泛型列表转换器,以免实现特定的“列表到列表”转换:

// Non-nullable to Non-nullable
interface ListMapper<I, O>: Mapper<List<I>, List<O>>class ListMapperImpl<I, O>(private val mapper: Mapper<I, O>
) : ListMapper<I, O> {override fun map(input: List<I>): List<O> {return input.map { mapper.map(it) }}
}
// Nullable to Non-nullable
interface NullableInputListMapper<I, O>: Mapper<List<I>?, List<O>>class NullableInputListMapperImpl<I, O>(private val mapper: Mapper<I, O>
) : NullableInputListMapper<I, O> {override fun map(input: List<I>?): List<O> {return input?.map { mapper.map(it) }.orEmpty()}
}
// Non-nullable to Nullable
interface NullableOutputListMapper<I, O>: Mapper<List<I>, List<O>?>class NullableOutputListMapperImpl<I, O>(private val mapper: Mapper<I, O>
) : NullableOutputListMapper<I, O> {override fun map(input: List<I>): List<O>? {return if (input.isEmpty()) null else input.map { mapper.map(it) }}
}

注:在这篇文章中我展示了如何使用简单的函数式编程,以更少的样板代码实现相同的功能。

3. 你需要为每个数据源建立独立模型

假设在网络和数据库中使用同一个模型:

@Entity(tableName = "Product")
data class ProductDTO(@PrimaryKey                @ColumnInfo(name = "id")    @SerializedName("id")val id: String?,@ColumnInfo(name = "name")@SerializedName("name")val name: String?,@ColumnInfo(name = "nowPrice")@SerializedName("nowPrice")val nowPrice: Double?,@ColumnInfo(name = "wasPrice")@SerializedName("wasPrice")val wasPrice: Double?
)

刚开始你可能会认为这比使用两个模型开发起来要快得多,但是你注意到它的风险了吗?

如果没有,我可以为你列出一些:

  • 你可能会缓存不必要的内容。
  • 在响应中添加新字段将需要变更数据库(除非添加@Ignore注解)。
  • 所有不应当在请求中发送的字段都需要添加@Transient注解。
  • 除非使用新字段,否则必须要求网络和数据库中的同名字段使用相同的数据类型(例如你无法解析网络响应中的字符串nowPrice并缓存双精度浮点数nowPrice)。

如你所见,这种方法最终将比独立模型需要更多的维护工作。

4. 你应该只缓存所需内容

如果要显示存储在远程目录中的产品列表,并且对本地保存的愿望清单中的每个产品显示经典的心形图标。

对于这个需求,需要:

  • 获取产品列表。
  • 检查本地存储,确认产品是否在愿望清单中。

这个领域模型很像前面的例子,添加了一个新字段表示产品是否在愿望清单中:

// Entity
data class Product( val id: String,val name: String,val price: Price,val isFavourite: Boolean
) {// Value objectdata class Price( val nowPrice: Double,val wasPrice: Double) {companion object {val EMPTY = Price(0.0, 0.0)}}
}

网络模型也和前面的示例类似,数据库模型则不再需要。

对于本地的愿望清单,可以将产品id保存在SharedPreferences中。不要使用数据库把简单的事情复杂化。

最后是仓库代码:

class ProductRepositoryImpl(private val productApiService: ProductApiService,private val productDataMapper: Mapper<DataProduct, Product>,private val productPreferences: ProductPreferences
) : ProductRepository {override fun getProducts(): Single<Result<List<Product>>> {return productApiService.getProducts().map {when(it) {is Result.Success -> Result.Success(mapProducts(it.value))is Result.Failure -> Result.Failure<List<Product>>(it.throwable)}}}private fun mapProducts(networkProductList: List<NetworkProduct>): List<Product> {return networkProductList.map { productDataMapper.map(DataProduct(it, productPreferences.isFavourite(it.id)))}}      
}

其中依赖的类定义如下:

// A wrapper for handling failing requests
sealed class Result<T> {data class Success<T>(val value: T) : Result<T>()data class Failure<T>(val throwable: Throwable) : Result<T>()
}// A DataSource for the SharedPreferences
interface ProductPreferences {fun isFavourite(id: String?): Boolean
}// A DataSource for the Remote DB
interface ProductApiService {fun getProducts(): Single<Result<List<NetworkProduct>>>fun getWishlist(productIds: List<String>): Single<Result<List<NetworkProduct>>>
}// A cluster of DTOs to be mapped into a Product
data class DataProduct(val networkProduct: NetworkProduct,val isFavourite: Boolean
)

现在,如果只想获取愿望清单中的产品要怎么做呢?实现方式是类似的:

class ProductRepositoryImpl(private val productApiService: ProductApiService,private val productDataMapper: Mapper<DataProduct, Product>,private val productPreferences: ProductPreferences
) : ProductRepository {override fun getWishlist(): Single<Result<List<Product>>> {return productApiService.getWishlist(productPreferences.getFavourites()).map {when (it) {is Result.Success -> Result.Success(mapWishlist(it.value))is Result.Failure -> Result.Failure<List<Product>>(it.throwable)}}}private fun mapWishlist(wishlist: List<NetworkProduct>): List<Product> {return wishlist.map {productDataMapper.map(DataProduct(it, true))}}
}

5. 后记

我多次熟练使用这种模式,我想它是一个时间节约神器,尤其在大型项目中。

然而我多次看到开发者使用这种模式仅仅是因为“不得不”,而非他们了解这种模式的真正优势。

希望你觉得这篇文章有趣也有用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/230678.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

部署清华ChatGLM-6B(Linux版)

引言 前段时间,清华公布了中英双语对话模型 ChatGLM-6B,具有60亿的参数,初具问答和对话功能。最!最!最重要的是它能够支持私有化部署,大部分实验室的服务器基本上都能跑起来。因为条件特殊,实验室网络不通,那么如何进行离线部署呢? 「部署环境」:CUDA Version 11.0,…

【JavaEE进阶】 关于Spring mvc 响应

文章目录 &#x1f38d;序言&#x1f333; 返回静态⻚⾯&#x1f332;RestController 与 Controller 的关联和区别&#x1f334;返回数据 ResponseBody&#x1f38b;返回HTML代码⽚段&#x1f343;返回JSON&#x1f340;设置状态码&#x1f384;设置Header&#x1f6a9;设置Con…

代码随想录算法训练营第五十七天|647. 回文子串、516.最长回文子序列、动态规划总结篇

代码随想录 (programmercarl.com) 647. 回文子串 1.dp数组及下标含义 我们在判断字符串S是否是回文&#xff0c;那么如果我们知道 s[1]&#xff0c;s[2]&#xff0c;s[3] 这个子串是回文的&#xff0c;那么只需要比较 s[0]和s[4]这两个元素是否相同&#xff0c;如果相同的话&…

斯坦福和 Meta学者发现Gemini在常识推理任务中有较强潜力;初学者GPT:Ai和LLM资源

&#x1f989; AI新闻 &#x1f680; 斯坦福和 Meta学者发现Gemini在常识推理任务中有较强潜力 摘要&#xff1a;斯坦福和Meta的学者发表论文为Gemini正名&#xff0c;他们发现之前对Gemini的评估并不能完全捕捉到其真正的常识推理潜力。他们设计了需要跨模态整合常识知识的任…

鸿蒙开发第一天

一、开发准备工作 1、开发工具的安装 1&#xff09;下载地址&#xff1a;https://developer.huawei.com/consumer/cn/deveco-studio/ 2&#xff09;查询API文档链接&#xff1a;https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V2/syscap-00000014080893…

m3u8网络视频文件下载方法

在windows下&#xff0c;使用命令行cmd的命令下载m3u8视频文件并保存为mp4文件。 1.下载ffmpeg&#xff0c;访问FFmpeg官方网站&#xff1a;https://www.ffmpeg.org/进行下载 ffmpeg下载&#xff0c;安装&#xff0c;操作说明 https://blog.csdn.net/m0_53157282/article/det…

PACC:数据中心网络的主动 CNP 生成方案

PACC&#xff1a;数据中心网络的主动 CNP 生成方案 文章目录 PACC&#xff1a;数据中心网络的主动 CNP 生成方案PACC算法CNP数据结构PACC参数仿真结果参考文献 PACC算法 CNP数据结构 PACC参数 仿真结果 PACC Hadoop Load0.2 的情况&#xff1a; PACC Hadoop Load0.4 的情况&a…

基于机器视觉的害虫种类及计数检测研究-人工智能项目-附代码

概述 农业与民生和经济发展息息相关&#xff0c;对农业发展科学化的关注既是民生需求&#xff0c; 也是经济稳步发展的迫切需求。病虫害是影响农作物生长的重要因素&#xff0c;对农作物的产量和品质都能造成无法估计的损害。 - 针对目前广大农业产区农业植保人员稀缺、病虫害…

Docker与虚拟机的比对

在Windows操作系统上的对比&#xff1a; 但是官方还是建议我们尽量不要将Docker直接安装到Windows操作系统上。

2024第一篇: 架构师成神之路总结,你值得拥有

大家好&#xff0c;我是冰河~~ 很多小伙伴问我进大厂到底需要怎样的技术能力&#xff0c;经过几天的思考和总结&#xff0c;终于梳理出一份相对比较完整的技能清单&#xff0c;小伙伴们可以对照清单提前准备相关的技能&#xff0c;在平时的工作中注意积累和总结。 只要在平时…

达梦数据库安装超详细教程(小白篇)

文章目录 达梦数据库一、达梦数据库简介二、达梦数据库下载三、达梦数据库安装1. 解压2. 安装 四、初始化数据库五、DM管理工具 达梦数据库 一、达梦数据库简介 ​ 达梦数据库管理系统是达梦公司推出的具有完全自主知识产权的高性能数据库管理系统&#xff0c;简称DM。 达梦数…

(湖科大教书匠)计算机网络微课堂(下)

第四章、网络层 网络层概述 网络层主要任务是实习网络互连&#xff0c;进而实现数据包在各网络之间的传输 因特网使用TCP/IP协议栈 由于TCP/IP协议栈的网络层使用网际协议IP&#xff0c;是整个协议栈的核心协议&#xff0c;因此TCP/IP协议栈的网络层常称为网际层 网络层提供…

Java 流程控制语句

程序设计中规定的三种流程结构&#xff0c;即&#xff1a; 顺序结构 程序从上到下逐行地执行&#xff0c;中间没有任何判断和跳转 分支结构 根据条件&#xff0c;选择性地执行某段代码 有 if…else 和 switch-case 两种分支语句 循环结构 根据循环条件&#xff0c;重复性的执…

Yapi安装配置(CentOs)

环境要求 nodejs&#xff08;7.6) mongodb&#xff08;2.6&#xff09; git 准备工作 清除yum命令缓存 sudo yum clean all卸载低版本nodejs yum remove nodejs npm -y安装nodejs,获取资源,安装高版本nodejs curl -sL https://rpm.nodesource.com/setup_8.x | bash - #安装 s…

交换机03_基本配置

一、思科设备的命令行基础 1、进入设备的命令行界面 设备支持命令行 去查看设备上的接口&#xff0c;是否有console口需要有console线 右击此电脑设备管理器需要通过超级终端软件进行连接&#xff0c;如putt、secret CRT、xshell等软件 &#xff08;1&#xff09;思科模拟器…

在 Linux 中使用 cat 命令

cat 命令用于打印文本文件的文件内容。至少&#xff0c;大多数 Linux 用户都是这么做的&#xff0c;而且没有什么问题。 cat 实际上代表 “连接(concatenate)”&#xff0c;创建它是为了 合并文本文件。但只要有一个参数&#xff0c;它就会打印文件内容。因此&#xff0c;它是用…

ASP.NETCore WebAPI 入门 杨中科

ASP.NETCore WebAPI入门1 回顾 mvc开发模式 前端代码和后端代码是混在一个项目之中 WEB API 1、什么是结构化的Http接口。Json。 2、Web API项目的搭建。 3、Web API项目没有Views文件夹。 4、运行项目&#xff0c;解读代码结构。 5、【启用OpenAPI支持】→>swagger,在界…

基于SSM的新闻网站

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

性能测试之(六):JMeter 元件

元件&#xff08;多个类似功能组件的容器&#xff09; 组件&#xff1a;封装的方法&#xff0c;比如取样器中的发送请求的方法 一、常见的元件 1、取样器&#xff1a;发送请求2、逻辑处理&#xff1a;控制语句执行顺序3、前置处理器&#xff1a;在请求&#xff08;取样器&…

Linux内核中断

Linux内核中断 ARM里当按下按键的时候&#xff0c;他首先会执行汇编文件start.s里面的异常向量表里面的irq,在irq里面进行一些操作。 再跳转到C的do_irq(); 进行操作&#xff1a;1&#xff09;判断中断的序号&#xff1b;2&#xff09;处理中断&#xff1b;3&#xff09;清除中…