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.mzph.cn/news/600793.shtml

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

相关文章

pytest安装失败,报错Could not find a version that satisfies the requirement pytest

问题 安装pytest失败&#xff0c;尝试使用的命令有 pip install pytest pip3 install pytest pip install -U pytest pip install pytest -i https://pypi.tuna.tsinghua.edu.cn/simple但是都会报同样的错&#xff1a; 解决方案 发现可能是挂了梯子的原因&#xff0c;关掉…

代码随想录算法训练营Day20|654.最大二叉树、617.合并二叉树、700.二叉搜索树中的搜索、98.验证二叉搜索树

目录 654.最大二叉树 前言 递归法 617.合并二叉树 前言 递归法 700.二叉搜索树中的搜索 前言 递归法 递归法 98.验证二叉搜索树 前言 递归法 迭代法 总结 654.最大二叉树 题目链接 文章链接 前言 本题延续昨天最后一题&#xff0c;依然是一道构造二叉树的题目…

烟花燃放如何管控?智能分析网关V4烟火检测保障烟火安全

一、方案背景 随着元旦佳节的热潮退去&#xff0c;春节也即将来临&#xff0c;在众多传统的中国节日里&#xff0c;烟花与烧纸祭祀都是必不可少的&#xff0c;一方面表达了人们对节日的庆祝的期许&#xff0c;另一方面也是一种对故者思念的寄托。烟花爆竹的燃放不仅存在着巨大的…

Node.js中的模块,常用模块具体代码示例

核心模块&#xff1a;https://blog.csdn.net/kkkys_kkk/article/details/135409851?spm1001.2014.3001.5501 目录 第三方模块 代码示例 Express示例 Lodash示例 MongoDB示例 Async示例 Request示例 发送GET 发送POST请求 自定义模块 创建步骤 常见示例 导出一个函数&a…

【PHP】TP5 使用模型一对一关联查询,条件筛选及字段过滤

目录 方法一&#xff1a;使用Eloquent ORM的with关联查询 方法二&#xff1a;使用JOIN进行查询 方法一&#xff1a;使用Eloquent ORM的with关联查询 在 ThinkPHP5 中&#xff0c;可以使用模型关联和条件查询来实现一对一关联查询。以下是一个示例&#xff1a; 假设有两个表&a…

跑通大模型领域的 hello world

跑通书生浦语大模型的 3 个趣味 demo&#xff08;InternLM-Chat-7B 智能对话、Lagent工具调用解简单数学题、浦语灵笔多模态图文创作和理解&#xff09;视频和文档。 1、两个框架 InternLM 是⼀个开源的轻量级训练框架&#xff0c;旨在⽀持⼤模型训练⽽⽆需⼤量的依赖。 Lage…

Underactuated Robotics - 欠驱动机器人学(一)- 全驱动与欠驱动系统

系列文章目录 前言 如今的机器人行动过于保守&#xff0c;只能完成机械性能所能完成的一小部分任务&#xff0c;实现一小部分性能。在某些情况下&#xff0c;我们仍然从根本上受限于在结构化工厂环境中成熟的刚性机械臂控制技术&#xff0c;在这种环境中&#xff0c;可以使用大…

烧录FRU方法

烧录FRU ipmitool远程命令示例: ipmitool -I lanp -H 127.0.0.1 -U admin -P admin write FRUID FRUfilename 1、修改Chassis PN ipmitool fru edit 0 field c 0 01234567892、修改 Board PN ipmitool fru edit 0 field b 3 01234567893、修改 Product PN ipmitool fru ed…

docker一键安装命令

docker一键安装命令 curl -fsSL https://get.docker.com | bash -s docker开机自动运行 systemctl start docker systemctl enable docker配置镜像仓库 sudo vim /etc/docker/daemon.json增加或修改以下配置内容 {"registry-mirrors": ["https://dockerprox…

将文本文件导入Oracle数据库的简便方法:SQL Loader Express

需求 我有一个文本文件dbim.txt&#xff0c;是通过alert log生成的&#xff0c;内容如下&#xff1a; 2020-09-11 2020-09-11 ... 2023-12-03 2023-12-03 2023-12-26我已经在Oracle数据库中建立了目标表&#xff1a; create table dbim(a varchar(16));我想把日志文件导入Or…

C++之STL库简介

目录 一、STL&#xff08;Standard Template Library&#xff0c;标准模板库&#xff09; 二、容器&#xff08;Containers&#xff09; 1.vector&#xff08;动态数组&#xff09; 2.list&#xff08;双向链表&#xff09; 3.deque&#xff08;双端队列&#xff09; 4.st…

用友U8 Cloud smartweb2.RPC.d XXE漏洞复现

0x01 产品简介 用友U8 Cloud 提供企业级云ERP整体解决方案,全面支持多组织业务协同,实现企业互联网资源连接。 U8 Cloud 亦是亚太地区成长型企业最广泛采用的云解决方案。 0x02 漏洞概述 用友U8 Cloud smartweb2.RPC.d接口处存在 XXE漏洞,攻击者可通过该漏洞获取敏感文件…

软件工程:用例图相关知识和多实例分析

目录 一、用例图相关知识 1. 基本介绍 2. 常用符号 二、用例图实例分析 1. 新闻管理系统 2. 医院病房监护系统 3. 实验上机安排系统 4. 远程网络教学系统 一、用例图相关知识 1. 基本介绍 用例图&#xff08;use case diagram&#xff09;是用户与系统交互的最简表示…

L1-084:拯救外星人

你的外星人朋友不认得地球上的加减乘除符号&#xff0c;但是会算阶乘 —— 正整数 N 的阶乘记为 “N!”&#xff0c;是从 1 到 N 的连乘积。所以当他不知道“57”等于多少时&#xff0c;如果你告诉他等于“12!”&#xff0c;他就写出了“479001600”这个答案。 本题就请你写程序…

条款35:考虑virtual函数以外的其它选择

1.前言 假设我们在写一个3D游戏软件&#xff0c;打算为游戏内的人物设计一个继承体系。游戏内容属于暴力砍杀类型&#xff0c;游戏中的角色被伤害或其它因素导致健康状态下降的情况是一个常见属性。因此设计一个成员函数healthValue&#xff0c;它会返回一个整数&#xff0c;表…

2024年天津体育学院专升本专业考试考生入场及考前须知

天津体育学院2024年高职升本科招生专业考试考生考前须知 一、考生入场及考试要求 1.考生于1月6日笔试考试当天&#xff0c;根据考试时间提前30分钟到达天津体育学院新校区东门&#xff0c;凭专业考试准考证、有效身份证原件&#xff0c;经查验合格后方可允许进入学校。 2.笔…

Linux 中 EXPORT_SYMBOL宏详解

Linux 中 EXPORT_SYMBOL宏详解 大家好&#xff0c;我是免费搭建查券返利机器人赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;在今天的文章中&#xff0c;我们将深入研究C/C编程中一个关键的宏——EXPORT_SYMBOL&…

PLC分段传送带控制示例

一、为了节约用电&#xff0c;将长长的传送带进行分段&#xff0c;哪断上有物品&#xff0c;哪断才运行 二、每一断末尾都有传感器&#xff0c;传感器能感受到物体有没有到传送带的末尾 三、这个传感器是接近开关 四、控制流程 五、IO地址分配 六、按下启动按钮后&#xff0c;…

问题 C: 活动选择

题目描述 学校在最近几天有n个活动&#xff0c;这些活动都需要使用学校的大礼堂&#xff0c;在同一时间&#xff0c;礼堂只能被一个活动使。由于有些活动时间上有冲突&#xff0c;学校办公室人员只好让一些活动放弃使用礼堂而使用其他教室。    现在给出n个活动使用礼堂的起…

Apache网页优化

本章主要介绍如何对Apache网页进行优化 Apache 网页压缩Apache 网页缓存Apache 隐藏版本信息Apache 网页防盗链 目录 1、网页压缩与缓存 1.1、网页压缩 &#xff08;1&#xff09;gzip介绍 &#xff08;2&#xff09;HTTP压缩的过程 &#xff08;3&#xff09;Apache的…