Android 架构 UseCase最佳实践

news2024/9/23 19:18:31

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(/*...*/): Image

    fun 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 function
    suspend operator fun invoke(): Boolean {}

    // normal names
    suspend fun send():  Boolean {}
}


// --------------Usage--------------------

class HomeViewModel(): ... {

    fun startPayment(...) {
      sendPaymentUseCase() // using invoke
      sendPaymentUseCase.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()
}

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

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

相关文章

《征服数据结构》双向链表

摘要&#xff1a; 1&#xff0c;双链表的介绍 2&#xff0c;双链表的用途 3&#xff0c;双链表的节点插入和删除 1&#xff0c;双链表的介绍 前面我们讲过单链表&#xff0c;单链表的特点就是只能往后访问不能往前访问。单链表一般在面试中用的比较多&#xff0c;比如删除倒数第…

【数据分析面试】55. 寻找双词组 (Python)

题目&#xff1a; 寻找双词组 &#xff08;Python&#xff09; 编写一个名为 find_bigrams 的函数&#xff0c;该函数接收一个句子或段落的字符串&#xff0c;并按顺序返回其所有双词组的列表。 注意&#xff1a; 双词组是指连续的两个单词。 示例&#xff1a; 输入&#x…

gitLab 使用tortoiseGit 克隆新项目 一直提示tortoiseGitPlink输入密码 输完也不生效

问题描述&#xff1a;准备用TortoiseGit拉取gitlab上一个新项目代码&#xff0c;出现tortoiseGitPlink提示让输入密码&#xff0c;输入后又弹出&#xff0c;反复几次&#xff0c;无法down下来代码。 解决方案&#xff1a; 1.找到PuTTYgen工具&#xff0c;打开 2. 点击load 按钮…

Docker是什么?使用场景作用及Docker的安装和启动详解

目录 Docker是什么&#xff1f; Docker的发展 Docker的安装 Docker使用 Docker的运行机制 第一个Docker容器 进入Docker容器 客户机访问容器 Docker是什么&#xff1f; Docker 是一个开源的应用容器引擎&#xff0c;基于 Go 语言 并遵从 Apache2.0 协议开源。 Docker …

网络安全防御之下一代防火墙部署思路分享

随着企业在数字化转型过程中不断深化&#xff0c;为了促进业务快速且安全地推出和更新&#xff0c;企业所采用的应用架构和部署方式经历了显著的演进&#xff1a;它们从单一应用转变为分层架构&#xff0c;进而发展为微服务架构&#xff1b;同时部署方式也由传统的本地部署进化…

区间选点问题-贪心-C++

问题&#xff1a; 给定 &#x1d441; 个闭区间 [ai,bi]&#xff0c;请你在数轴上选择尽量少的点&#xff0c;使得每个区间内至少包含一个选出的点。 输出选择的点的最小数量。 位于区间端点上的点也算作区间内。 输入格式 第一行包含整数 &#x1d441;&#xff0c;表示区间数…

这样问问题, 客户才会下单!

和供应商沟通时&#xff0c;提问方式至关重要。如何提问是外贸交流的关键&#xff0c;而针对客户的合适提问更是促成交易的秘诀。前不久&#xff0c; 我有一个客户想要一个不属于我们行业的新品&#xff0c; 于是我准备去阿里巴巴找找供应商。但因为我对该领域并不了解&#xf…

MYSQL 使用子查询id数据做条件更新

举例&#xff0c;我想查出某个表里面符合条件的前5条数据的id 做数据更新 &#xff1a; 想法 报错 更正写法 &#xff1a; 示例&#xff1a; UPDATE t_authorisation_record ar1JOIN (SELECT IDFROM t_authorisation_recordWHERE device_company_name JCcccORDER BY id ASCLI…

Python代码:二十一、增加派对名单(二)

1、题目 描述 为庆祝驼瑞驰在牛爱网找到合适的对象&#xff0c;驼瑞驰通过输入的多个连续字符串创建了一个列表作为派对邀请名单&#xff0c;在检查的时候发现少了他最好的朋友“Allen”的名字&#xff0c;因为是最好的朋友&#xff0c;他想让这个名字出现在邀请列表的最前面…

边缘计算网关的用途及其使用方法-天拓四方

在数字化日益深入的今天&#xff0c;边缘计算网关作为一种重要的设备&#xff0c;正在越来越多地被应用于各种场景中。它不仅能够提升数据处理的速度和效率&#xff0c;还能在降低网络延迟的同时确保数据的安全性。本文将详细介绍边缘计算网关的用途及其使用方法&#xff0c;帮…

linux安装mysql后,配置mysql,并连接navicate软件

Xshell连接登陆服务器 输入全局命令 mysql -u root -p 回车后&#xff0c;输入密码&#xff0c;不显示输入的密码 注意mysql服务状态&#xff0c;是否运行等 修改配置文件my.cnf&#xff0c;这里没找到就找my.ini&#xff0c;指定有一个是对的 find / -name my.cnf 接下…

04_前端三大件JS

文章目录 JavaScript1.JS的组成部分2.JS引入2.1 直接在head中通过一对script标签定义脚本代码2.2创建JS函数池文件&#xff0c;所有html文件共享调用 3.JS的数据类型和运算符4.分支结构5.循环结构6.JS函数的声明7.JS中自定义对象8.JS_JSON在客户端使用8.1JSON串格式8.2JSON在前…

2024年zoom会议受主持人账户限制影响,无法加入会议。(错误代码13215)

问题一、老师&#xff0c;你好!我的zoom账户&#xff0c;刚开始注册后可以登录&#xff0c;但是现在登录不了了。代码1044。其次&#xff0c;我如果通过网页版设置会议号&#xff0c;别人也加入不了。代码13215。 这两个问题一般会同时出现。登录失败。(错误代码:1044)一般是创…

如何自动识别图片上的文字?一键转文本方法分享

如何自动识别图片上的文字&#xff1f;自动识别图片上的文字&#xff0c;即OCR技术&#xff0c;已经取得显著进步&#xff0c;并在多个领域得到广泛应用。它可以极大地提高我们处理文本信息的效率&#xff0c;无论是从文档扫描、图片识别&#xff0c;还是到自动翻译和数据录入&…

【干货】如何用SaaS商城助力全球化布局

在全球化的大潮中&#xff0c;企业如何迅速而有效地拓展海外市场&#xff0c;成为了众多企业家和管理者关注的焦点。随着技术的发展&#xff0c;SaaS&#xff08;软件即服务&#xff09;商城作为一种创新的商业模式&#xff0c;为企业全球化布局提供了强有力的支持。saas商城如…

Java整合EasyExcel实战——2 导出复杂表头

详情代码 实体类 Data public class ComplexHeadData {ExcelProperty({"主标题", "字符串标题"})private String string;ExcelProperty({"主标题", "日期标题"})private Date date;ExcelProperty({"主标题", "数字标…

学习云安全?就来亚马逊云科技中国峰会安全动手实验专场

一年一度的云计算春晚“亚马逊云科技中国峰会”明天就正式开始了&#xff0c;大会的注册将在5月28日今天晚18:00前停止报名&#xff0c;还没有报名的小伙伴请抓紧了&#xff01;可以通过下方二维码注册大会。 小李哥也来提前带大家了解下本次大会中云安全相关的动手实验和活动&…

【NumPy】全面解析NumPy的sort函数:高效排序算法与应用技巧

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

基于瑞萨RA6M5的自控衣橱

1. 主控转接板原理图和PCB设计 2. 屏幕界面设计 3. 程序设计 4. QT设计 QT设计&#xff0c;读取MQTT数据&#xff0c;在QT上显示衣橱内部的温度&#xff0c;湿度情况&#xff0c;且能够控制衣橱的开关门&#xff0c;开关灯等。 5. 实物演示 瑞萨

Scala编程基础3 数组、映射、元组、集合

Scala编程基础3 数组、映射、元组、集合 小白的Scala学习笔记 2024/5/23 14:20 文章目录 Scala编程基础3 数组、映射、元组、集合apply方法数组yield 数组的一些方法映射元组数据类型转换求和示例拉链集合flatMap方法 SetHashMap apply方法 可以new&#xff0c;也可以不new&am…