Android 架构 UseCase最佳实践

2024-05-29 03:44

本文主要是介绍Android 架构 UseCase最佳实践,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Android 架构 UseCase最佳实践

  • 前言
  • UseCase 的职责
  • UseCase 的命名
  • UseCase 的线程安全
  • UseCase 的签名依赖
  • UseCase 的引用透明
  • UseCase 的接口抽象

前言

Android 官方的最新架构中引入了 Domain (网域层 or 领域层),Domain Layer 由一个个 UseCase 组成。但是由于团队套用官方新架构后没有正确地定义 UseCase,无法发挥 Domain Layer 应有的架构价值。

本文就带大家一起梳理 UseCase 常见的使用误区和最佳实践。

UseCase 的职责

一句话概括,UseCase 用来封装可复用的单一业务逻辑。这里的两个关键词一个是单一、一个是业务逻辑

首先 UseCase 应该用来定义一段 Logic,这段 Logic 与 UI 以及 Data 的访问方式无关,是独立于 UI 和 Data 之外的 Business。

我们都知道良好的架构应该做到关注点分离,即表现层和数据层的解耦。领域层一定程度扮演着这个解耦的角色,但是如果仅仅是为了做隔离和解耦,只要定义好 ViewModel 即可,没必要引入 Domain 和 UseCase 这一新的层级概念,所以官方文档也说了 Domina 层是可选的。

UseCase 如果存在,则其逻辑应该有一定的复杂度,这样才有被“封装”的价值。举一个例子,一个支付相关的业务逻辑,应该包含事务的发起和结束以及,以及事务过程中的异常处理:

class SendPayment(private val repo: PaymentRepo) {suspend operator fun invoke(amount: Double,checkId: String,): Boolean {val transactionId = repo.startTransaction(params.checkId)repo.sendPayment(amount = params.amount,checkId = params.checkId,transactionId = transactionId)return repo.finalizeTransaction(transactionId)}
}

此外,一个 UseCase 应该是单一职责,甚至可以就是一个 Functioin,这样才能以更小颗粒度被复用,提升复用范围也更易于测试。一个检验 UseCase 是否职责单一的方法是看它的命名是否语义明确,好的命名应该是一个具体动作。

一个名词命名的 UseCase 很难做到职责单一,如 GalleryUseCase,这类对象往往基于 OOP 思想设计,内部多个成员方法。经验告诉我们,方法越多,单一方法的价值越低,有的多方法的 UseCase 没提供什么业务价值,甚至沦为了一个 Repository 的 Wrapper。

好的 UseCase 只要完成一件有价值的业务即可,Repository 只是它完成业务工具。价值体现在业务逻辑具备一定的复杂度,何为“复杂”,前面已经举例了。

下面是 UseCase 是否职责单一的正反例子

// DON'T ❌ - 名词命名,
// 一般是OOP思想下的产物,功能多,容易违背单一职责
class GalleryUseCase @Inject constructor(/*...*/
) {fun saveImage(file: File)fun downloadFileWithSave(/*...*/)fun downloadImage(/*...*/): Imagefun getChatImageUrl(messageID: String)
}// DON'T ❌ - 只是一个 Repository 的包装器
class GetSomethingUseCase @Inject constructor(private val repository: ChannelsRepository,
) {suspend operator fun invoke(): List<String> = repository.getSomething()
}// DO ✅ - 动词命名,单一职责
class SaveImageUseCase @Inject constructor(/*...*/
) {operator fun invoke(file: File): Single<Boolean>// 这里虽然有多个方法,但其实是重载方法,职责上仍然是单一的operator fun invoke(path: String): Single<Boolean>
}class GetChatImageUrlByMessageIdUseCase() {operator fun invoke(messageID: String): Url {...}
}

单一职责下的 UseCase 可以更好地被其他 UseCase 使用,官方文档也鼓励通过 UseCase 的组合调用实现更复杂的业务逻辑。

UseCase 的命名

前面提过,UseCase 的命名通常是一个语义明确的动作:动词(一般现在时) + 名词() + UseCase

例如 FormatDateUseCase, GetChatUserProfileUseCase, RemoveDetektRulesUseCase 等。UseCase 类中的函数可以直接使用 invoke 操作符重载,也可以给一个动词作为名字

class SendPaymentUseCase(private val repo: PaymentRepo) {// using operator functionsuspend operator fun invoke(): Boolean {}// normal namessuspend fun send():  Boolean {}
}// --------------Usage--------------------class HomeViewModel(): ... {fun startPayment(...) {sendPaymentUseCase() // using invokesendPaymentUseCase.send() using normal functions}
}

invoke 操作符更优于常规函数,因为:

  • 开发者只要给 UseCase 一个合适的命名即可,无需考虑函数的命名
  • 调用起来非常简单
  • 便于重载,当增加新的非 invoke 方法时也比较容易被察觉,避免单一职责的劣化

UseCase 的线程安全

官方文档提到 UseCase 应该是 Main-safe 的,即可以在主线程安全的调用,其中的耗时处理应该自动切换到后台线程。

// DON'T ❌ - add 和 sort 都是耗时操作,不能直接在主线程执行
class AUseCase @Inject constructor() {suspend operator fun invoke(): List<String> {val list = mutableListOf<String>()repeat(1000) {list.add("Something $it")}return list.sorted()}
}// DO ✅ - 主线程调用下,也不用担心性能问题
class AUseCase @Inject constructor(// or default dispatcher@IoDispatcher private val dispatcher: CoroutineDispatcher,
) {suspend operator fun invoke(): List<String> = withContext(dispatcher) {val list = mutableListOf<String>()repeat(1000) {list.add("Something $it")}list.sorted()}
}// DON'T ❌ - 避免过度切换线程
// Repository 应该也是 main safe 的,所以没必要再切换一次 Context, 直接调用节课
class AUseCase @Inject constructor(private val repository: ChannelsRepository,// or default dispatcher@IoDispatcher private val dispatcher: CoroutineDispatcher,
) {suspend operator fun invoke(): List<String> = withContext(dispatcher) {repository.getSomething()}
}

UseCase 的签名依赖

UseCase 应该是一段纯业务逻辑,它的函数签名(输入输出)不应该依赖 UI 或平台设备相关的依赖,包括 Context 类,这样才具备更好的可复用性。

此外,UseCase 的签名不应该以来 UI层的 Model,这会让 UseCase 沦为从 Data Model 到 UI Model 映射的工具,这是 ViewModel 的事情。

对于异常,UseCase 只需要返回 error code 类型而不是具体的 message,error messag 应该由 UI 基于 error code 生成。

// DON'T ❌ - 不应该依赖任何 Android 平台相关对象,甚至 Context
class AddToContactsUseCase @Inject constructor(@ApplicationContext private val context: Context,
) {operator fun invoke(name: String?,phoneNumber: String?,) {context.addToContacts(name = name,phoneNumber = phoneNumber,)
}

UseCase 的引用透明

如果将 UseCase 认为是一个函数,那么它最好具备一个纯函数的特性,内部不应该包含 mutable 的数据。

UseCase 本身不持有可监听的状态,它内部如果隐藏了可变数据,且在业务逻辑会受到内部可变数据的影响,会破坏 UseCase 的幂等性,在多场景复用时会出现相同输入但输出不同的情况。

// DON't ❌ 
class PerformeSomethingUseCase @Inject constructor() {val list = mutableListOf<String>()suspend operator fun invoke(): List<String> {repeat(1000) {list.add("Something $it")}return list.sorted()}
}

好的 UseCase 其唯一输入只会得到唯一输出,这被称为引用透明。

UseCase 的接口抽象

有一些文章会看到对 UseCase 做接口抽象和派生。

//定义 UseCase 接口
interface GetSomethingUseCase {suspend operator fun invoke(): List<String>
}//UseCase 派生类
class GetSomethingUseCaseImpl(private val repository: ChannelsRepository,
) : GetSomethingUseCase {override suspend operator fun invoke(): List<String> = repository.getSomething()
}

如上,定义 UseCase 的接口和对应实现,然后在 DI 容器中,可以通过 @Bind 提供实例注入。其实这种属于过度设计,单一职责的 UseCase 应该只有一个方法或一类重载方法,而且方法最好是纯函数逻辑,不依赖 UseCase 对象的任何状态,因此从这个角度讲,UseCase 可以是一个单例,直接使用 object 定义。

有时候 UseCase 需要动态依赖不同的 Repository,此时可以使用 class 定义 UseCase,按需实例化使用,或者在 DI 容器中被动注入,此时依赖的 Repository 可以从 DI 容器中自动获取。在 class 之外再定义一个 interface 必要性不大。

当然,还有一种使用 Kotlin 的 function interface 来定义 UseCase 的技巧,这里的 interface 主要目的不是为了抽象,而是想利用其单方法接口的特性,约束 UseCase class 里定义太多方法。

比如 , 像下面这样定义一个 UseCase 的单方法接口:

fun interface GetSomethingUseCase : suspend () -> List<String>

此时需要像下面这样实例化,只能定义一个方法,强制确保单一职责

val getSomethingUseCase = GetSomethingUseCase {repository.getSomething()
}

这篇关于Android 架构 UseCase最佳实践的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


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

相关文章

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired

MySQL分库分表的实践示例

《MySQL分库分表的实践示例》MySQL分库分表适用于数据量大或并发压力高的场景,核心技术包括水平/垂直分片和分库,需应对分布式事务、跨库查询等挑战,通过中间件和解决方案实现,最佳实践为合理策略、备... 目录一、分库分表的触发条件1.1 数据量阈值1.2 并发压力二、分库分表的核心技术模块2.1 水平分

Android协程高级用法大全

《Android协程高级用法大全》这篇文章给大家介绍Android协程高级用法大全,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友跟随小编一起学习吧... 目录1️⃣ 协程作用域(CoroutineScope)与生命周期绑定Activity/Fragment 中手

SpringBoot通过main方法启动web项目实践

《SpringBoot通过main方法启动web项目实践》SpringBoot通过SpringApplication.run()启动Web项目,自动推断应用类型,加载初始化器与监听器,配置Spring... 目录1. 启动入口:SpringApplication.run()2. SpringApplicat

Java整合Protocol Buffers实现高效数据序列化实践

《Java整合ProtocolBuffers实现高效数据序列化实践》ProtocolBuffers是Google开发的一种语言中立、平台中立、可扩展的结构化数据序列化机制,类似于XML但更小、更快... 目录一、Protocol Buffers简介1.1 什么是Protocol Buffers1.2 Pro

linux安装、更新、卸载anaconda实践

《linux安装、更新、卸载anaconda实践》Anaconda是基于conda的科学计算环境,集成1400+包及依赖,安装需下载脚本、接受协议、设置路径、配置环境变量,更新与卸载通过conda命令... 目录随意找一个目录下载安装脚本检查许可证协议,ENTER就可以安装完毕之后激活anaconda安装更

Android 缓存日志Logcat导出与分析最佳实践

《Android缓存日志Logcat导出与分析最佳实践》本文全面介绍AndroidLogcat缓存日志的导出与分析方法,涵盖按进程、缓冲区类型及日志级别过滤,自动化工具使用,常见问题解决方案和最佳实... 目录android 缓存日志(Logcat)导出与分析全攻略为什么要导出缓存日志?按需过滤导出1. 按

MySQL数据类型与表操作全指南( 从基础到高级实践)

《MySQL数据类型与表操作全指南(从基础到高级实践)》本文详解MySQL数据类型分类(数值、日期/时间、字符串)及表操作(创建、修改、维护),涵盖优化技巧如数据类型选择、备份、分区,强调规范设计与... 目录mysql数据类型详解数值类型日期时间类型字符串类型表操作全解析创建表修改表结构添加列修改列删除列

Python自定义异常的全面指南(入门到实践)

《Python自定义异常的全面指南(入门到实践)》想象你正在开发一个银行系统,用户转账时余额不足,如果直接抛出ValueError,调用方很难区分是金额格式错误还是余额不足,这正是Python自定义异... 目录引言:为什么需要自定义异常一、异常基础:先搞懂python的异常体系1.1 异常是什么?1.2

深入解析Java NIO在高并发场景下的性能优化实践指南

《深入解析JavaNIO在高并发场景下的性能优化实践指南》随着互联网业务不断演进,对高并发、低延时网络服务的需求日益增长,本文将深入解析JavaNIO在高并发场景下的性能优化方法,希望对大家有所帮助... 目录简介一、技术背景与应用场景二、核心原理深入分析2.1 Selector多路复用2.2 Buffer