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 object
    data 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 object
    data 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. 后记

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

liunx操作系统基础及进阶

一、基础入门 1、Linux系统简介 什么是Liunx&#xff1f; Linux在设计之初&#xff0c;是一个基于POSIX的多用户、多任务并且支持多线程和多CPU的操作系统&#xff0c;它是由世界各地成千上万的程序员设计和开发实现&#xff1b; 在当今社会&#xff0c;Linux 系统主要被应…

史上最细,13年老鸟总结-性能测试7大关键点,一篇打通...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、测试环境的鉴定…

MQTT基础下载使用

1.下载MQTT(MQTT官网) 下载完后在bin目录下启动cmd 控制台输入emqx start&#xff0c;注意&#xff0c;此时控制台是没有反应的&#xff0c;就回你个D&#xff1a;\EMQX。其实已经打开了。 打开桌面上的MQTTX 并新建连接 这是测试的数据 我订阅了一个test1的订阅 并且我发布…

跑步中位数

title: 跑步中位数 date: 2024-01-04 15:47:51 tags: 对顶堆 catefories: 算法进阶指南 题目大意 解题思路 动态维护中位数问题。可以建立两个二叉堆&#xff0c;一个大顶堆一个小顶堆&#xff0c;在依次读入整数序列的过程中&#xff0c;设当前序列长度为 M M M,我们始终保持…

软件测试之冒烟测试

一、什么是冒烟测试 这一术语源自硬件行业。对一个硬件或硬件组件进行更改或修复后&#xff0c;直接给设备加电。如果没有冒烟&#xff0c;则该组件就通过了测试。在软件中&#xff0c;“冒烟测试”这一术语描述的是在将代码更改嵌入到产品的源树中之前对这些更改进行验证的过…

通过聚道云软件连接器实现销帮帮软件与i人事软件的智能对接

客户介绍 某软件行业公司是一家专业从事软件技术服务、软件开发、应用解决方案、业务流程优化、专业服务的高科技企业。公司拥有一支经验丰富、技术精湛的服务团队&#xff0c;具备多年的软件开发和应用解决方案经验。他们不断追求技术的创新和进步&#xff0c;以满足客户不断…

CCF录用率怎么看?如何挑选合适的会议

写在前面 写此文是因为有同学问我如何确定自己能投稿的会议。首先&#xff0c;不建议直接用他人汇总好的数据&#xff08;截稿时间和录用率&#xff09;&#xff0c;如果遇到更新不及时的很有可能耽误自己的工作。 平常&#xff0c;我都会自己收集预计投稿时间的会议信息&…

phpstudy_pro 关于多版本php的问题

我在phpstudy中安装了多个PHP版本 我希望不同的网站可以对应不同的PHP版本&#xff0c;则在nginx配置文件中需要知道不同的PHP版本的监听端口是多少&#xff0c;如下图所示 然而找遍了php.ini配置&#xff0c;并未对listen进行设置&#xff0c;好奇是怎么实现不同的PHP监听不同…

炼石白小勇:免改造安全技术实现数据监管合规与有序流通

2023年9月15日&#xff0c;2023世界计算大会在湖南长沙开幕。在开幕论坛上&#xff0c;全国政协副主席、民建中央常务副主席秦博勇指出&#xff0c;当今世界正在经历一场更大范围、更深层次的科技革命和产业变革。湖南省委书记沈晓明在致辞中说&#xff0c;湖南将推动计算产业开…

python的课后练习总结4(for循环)

1&#xff0c;for循环 for 临时变量 in 序列: 重复执行的代码1 重复执行的代码2 ........... 遍历序列 字符串 我是中国人 列表 [‘星期一,星期二,星期三,星期四] 元组 (‘星期一,星期二,星期三,星期四&#xff09; 一&#xff0c;break 终止循环 二&#xff0c;con…

VS Code技巧汇总

VS Code技巧汇总 前言设置快捷键插件汇总环境搭建HTMLC/CPython 远程SSH连接被控端准备安装扩展配置SSH创建SSH连接打开终端窗口通过公钥连接SSH 前言 本文介绍VS Code的使用技巧&#xff0c;内容包含设置、快捷键、插件汇总、环境搭建、远程SSH连接、等等。 设置 中文界面 …

IDEA 每次新建工程都要重新配置 Maven的解决方案

文章目录 IDEA 每次新建工程都要重新配置 Maven 解决方案一、选择 File -> New Projects Setup -> Settingsfor New Projects…二、选择 Build,Execution,Deployment -> Build Tools -> Maven IDEA 每次新建工程都要重新配置 Maven 解决方案 DEA 每次新建工程都要…

完美解决Github 2fa二次验证问题

完美解决Github 2fa二次验证问题 原文阅读 https://onedayxyy.cn/docs/github-2fa 前言 你的 Github 账户可能被封禁! 教你应对 Github 最新的 2FA 二次验证! 无地区限制, 无额外设备的全网最完美方案 1、2FA 的定义 双因素身份验证 (2FA) 是一种身份和访管理安全方法&…

程序媛的mac修炼手册-- 终端shell的驾驭 zsh vs bash

进入终端(Terminal)为新下载的应用配置环境&#xff0c;是Mac生产力up up的关键一步&#xff0c;更是编程小白装大神的第一步。Fake it till you make it , 硅谷大神标准路径&#xff5e; shell的基本原理 为应用配置环境&#xff0c;相当于在应用和操作系统间架桥。由此&…

Flask入门教程

Flask入门教程 简介 Flask是由Armin ronacher于2010年用Python语言基于 Werkzeug 工具箱编写的轻量级Web开发框架。 特点 Flask只提供核心功能&#xff0c;其他几乎所有的功能都需要用到拓展&#xff0c;比如可以通过Flask-SQLAlchemy拓展对数据库进行操作等等。 核心 由…

LeetCode(33) 搜索旋转排序数组

整数数组 nums 按升序排列&#xff0c;数组中的值 互不相同 。 在传递给函数之前&#xff0c;nums 在预先未知的某个下标 k&#xff08;0 < k < nums.length&#xff09;上进行了 旋转&#xff0c;使数组变为 [nums[k], nums[k1], ..., nums[n-1], nums[0], nums[1], ..…

只需一招彻底解决SOLIDWORKS不显示缩略图预览

SOLIDWORKS缩略图能够让工程师便于识别想要打开的模型&#xff0c;但经常会有用户遇到在资源管理器中查看SOLIDWORKS文件时&#xff0c;仅显示SOLIDWORKS的图标&#xff0c;而没有相关文件的预览缩略图。 Windows文件夹选项设置 首先确保Windows文件夹选项设置&#xff0c;显…

【UEFI基础】EDK网络框架(通用函数和数据)

通用函数和数据 DPC DPC全称Deferred Procedure Call。Deferred的意思是“延迟”&#xff0c;这个DPC的作用就是注册函数&#xff0c;然后在之后的某个时刻调用&#xff0c;所以确实是有“延迟”的意思。DPC在UEFI的实现中包括两个部分。一部分是库函数DxeDpcLib&#xff0c;…

Java IO流介绍以及缓冲为何能提升性能

概念&#xff1a; 流是一种抽象概念&#xff0c;它代表了数据的无结构化传递。按照流的方式进行输入输出&#xff0c;数据被当成无结构的字节序或字符序列。从流中取得数据的操作称为提取操作&#xff0c;而向流中添加数据的操作称为插入操作。 Java IO 也称为IO流&#xff0c;…

中文大语言模型 Llama-2 7B(或13B) 本地化部署 (国内云服务器、GPU单卡16GB、中文模型、WEB页面TextUI、简单入门)

本文目的是让大家先熟悉模型的部署&#xff0c;简单入门&#xff1b;所以只需要很小的算力&#xff0c;单台服务器 单GPU显卡&#xff08;显存不低于12GB&#xff09;&#xff0c;操作系统需要安装 Ubuntu 18.04。 1 服务器&操作系统 1.1服务器的准备 准备一台服务器 单张…