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

news2024/11/15 21:26:52
  • 原文地址: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. 后记

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

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

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

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

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

相关文章

c# OpenCvSharp透视矫正参数调整器

透视矫正不够智能化&#xff0c;每次都要进行局部参数调整&#xff0c;不便于程序使用&#xff0c;程序流程还是那几个步骤&#xff1b; 1、读取图像、灰度化 2、高斯滤波 3、二值化 4、边缘检测 灰度化图 上个图看看经过调整透视矫正边缘检测结果我还是挺满意的 发现一个…

Nest 框架:解锁企业级 Web 应用开发的秘密武器(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

将ipad拓展为笔记本副屏

写在前面 对比过moonlight和spacedesk两种软件&#xff08;付费的更流畅&#xff0c;本人穷困暂不涉及&#xff09;&#xff0c;moonlight无线连接感觉更卡顿一些&#xff08;但是大多数人都觉得moonlight更丝滑&#xff0c;实践是检验真理的唯一标准&#xff0c;建议自己都试…

go执行静态二进制文件和执行动态库文件

目的和需求&#xff1a;部分go的核心文件不开源&#xff0c;例如验证&#xff0c;主程序核心逻辑等等 第一个想法&#xff0c;把子程序代码打包成静态文件&#xff0c;然后主程序执行 子程序 package mainimport ("fmt""github.com/gogf/gf/v2/os/gfile"…

UⅤ机用的滚珠丝杆套装

UⅤ机用的滚珠丝杆套装 上面是单丝杆UⅤ机内部。

ES(Elasticsearch)的基本使用

一、常见的NoSQL解决方案 1、redis Redis是一个基于内存的 key-value 结构数据库。Redis是一款采用key-value数据存储格式的内存级NoSQL数据库&#xff0c;重点关注数据存储格式&#xff0c;是key-value格式&#xff0c;也就是键值对的存储形式。与MySQL数据库不同&#xff0…

我的创作纪念日三年收获和感悟

机缘 我刚开始接触创作也是最近几年开始&#xff0c;当初就是希望自己的收获分享给大家&#xff0c;不仅使自己成长&#xff0c;也可以带着大家一起成长&#xff0c;独乐乐不如众乐乐&#xff0c;人都是自私的以前我都是看到好的知识文章都是自己藏起来&#xff0c;发现收获的…

Qt/C++编写视频监控系统82-自定义音柱显示

一、前言 通过音柱控件实时展示当前播放的声音产生的振幅的大小&#xff0c;得益于音频播放组件内置了音频振幅的计算&#xff0c;可以动态开启和关闭&#xff0c;开启后会对发送过来的要播放的声音数据&#xff0c;进行运算得到当前这个音频数据的振幅&#xff0c;类似于分贝…

初始数字孪生

文章目录 概念定义 架构框图 基本要求 功能要求 服务应用 参考文献 概念定义 数字孪生&#xff08;digital twin&#xff09;&#xff0c;是指具有保证物理状态和虚拟状态之间以适当速率和精度同步的数据连接的特定目标实体的数字化表达。 架构框图 数字孪生的整体架构&a…

玩转贝启科技BQ3588C开源鸿蒙系统开发板 —— DevEco Studio下载与安装

一、下载DevEco Studio IDE开发工具 1. 登录鸿蒙官网 网址为&#xff1a; ​​​​​​​华为HarmonyOS智能终端操作系统官网 | 应用设备分布式开发者生态 页面如下&#xff1a; 2. 搜索“DevEco Studio IDE” 点击右上角的“请输入关键词”&#xff0c;在其中搜索“DevEc…

Termius for Mac/Win:一款功能强大的终端模拟器、SSH 和 SFTP 客户端软件

随着远程工作和云技术的普及&#xff0c;对于高效安全的远程访问和管理服务器变得至关重要。Termius&#xff0c;一款强大且易用的终端模拟器、SSH 和 SFTP 客户端软件&#xff0c;正是满足这一需求的理想选择。 Termius 提供了一站式的解决方案&#xff0c;允许用户通过单一平…

钡铼分布式IO在玻璃制造中的实时数据采集与监控应用介绍

导读 玻璃行业多年来一直广泛使用 PLC 来帮助管理生产过程所需的精确材料比例&#xff0c;完全依赖其PLC进行数据采集与控制&#xff0c;并且大量依靠人工来操作&#xff0c;所以这些高成本推动了对成本较低的替代方案的需求。 场景描述 某玻璃厂生产的玻璃生产包括配料段、熔…

Excel 插件:ASAP Utilities Crack

ASAP Utilities是一款功能强大的 Excel 插件&#xff0c;填补了 Excel 的空白。在过去的 20 年里&#xff0c;我们的加载项已经发展成为世界上最受欢迎的 Microsoft Excel 加载项之一。 ASAP Utilities 中的功能数量&#xff08;300 多个&#xff09;可能看起来有点令人眼花缭乱…

2024.1.2 Redis 数据类型 Stream、Geospatial、HyperLogLog、Bitmaps、Bitfields 简介

目录 引言 Stream 类型 Geospatial 类型 HyperLogLog 类型 Bitmaps 类型 Bitfields 类型 引言 Redis 最关键&#xff08;应用广泛、频繁使用&#xff09;的五个数据类型 StringListHashSetZSet 下文介绍的数据类型一般适合在特定的场景中使用&#xff01; Stream 类型 St…

守护进程“独辟蹊径”

守护进程“独辟蹊径” 一、前言二、实际运用2.1 知识介绍2.2 单机库场景应用2.2.1 配置dmwatcher.ini2.2.2 注册后台守护服务2.2.3 配置dmmal.ini2.2.4 配置归档和守护OGUID2.2.5 开启mal2.2.6 启动守护2.2.7 测试dmserver异常退出 三、总结 DM技术交流QQ群&#xff1a;9401242…

alibabaCloud学习笔记01(小滴课堂)

微服务架构常见的核心组件 讲解业务微服务架构常见解决方案 讲解AlibabaCloud核心组件介绍 创建数据库。 建表&#xff1a; 添加数据&#xff1a; 再建个用户库&#xff1a; 建表&#xff1a; 插入数据&#xff1a; 创建订单库&#xff1a; 建表&#xff1a; 创建项目&#x…

【ikbp】数据可视化DataV

天天查询一些数据&#xff0c;希望来一个托拉拽的展示&#xff0c;部署体验一下可视化大屏 快速搭建快速查询实时更新简单易用 启动服务 数据可视化 静态查询 配置数据 过滤数据 分享

目标检测-One Stage-RetinaNet

文章目录 前言一、RetinaNet的网络结构和流程二、RetinaNet的创新点Balanced Cross EntropyFocal Loss 总结 前言 根据前文目标检测-One Stage-YOLOv2可以看出YOLOv2的速度和精度都有相当程度的提升&#xff0c;但是One Stage目标检测模型仍存在一个很大的问题&#xff1a; 前…

计算机组成原理-总线操作和定时(同步定时方式 异步定时方式 半同步通信 分离式通信)

文章目录 总览总线传输的四个阶段同步定时方式读命令小结 异步定时方式半同步通信分离式通信小结 总览 总线传输的四个阶段 同步定时方式 读命令 总线传输分为多个时钟周期 首先CPU在T1阶段发出地址信息&#xff0c;此时地址线信号改变 CPU在T2阶段发出读命令&#xff0c;此时…

【Fastadmin】系统配置自定义键值组件

目录 1.修改config表的extend字段为text类型&#xff0c;否则会出现长度不足报错 2.添加配置 3.参考代码 图1代码&#xff1a; 图2代码&#xff1a; 图3代码&#xff1a; html部分&#xff1a; js部分&#xff1a;assets/js/general/config.js 参考文档: FastAdmin是一…