第9章:UseCase层设计:单一职责的用例,UseCase的输入输出规范
好,咱们今天聊聊UseCase层。说实话,很多刚接触Clean架构的开发者,最容易搞混的就是这一层。我见过不少项目,UseCase里塞了几百行代码,什么网络请求、数据库操作、UI状态更新全搅在一起——那还不如不用Clean架构呢。
UseCase的核心思想其实很简单:一个用例只做一件事。你想想看,现实生活中的用例也是这样——"登录"是一个用例,"获取用户信息"是另一个用例,它们不应该混在一起。
9.1 什么是UseCase?
用大白话说,UseCase就是你的App里一个完整的业务操作。比如:
- 用户登录
- 获取商品列表
- 提交订单
- 刷新Token
每个操作都是一个独立的UseCase。它们不关心数据从哪来,也不关心UI怎么展示,只关心"这个业务逻辑该怎么执行"。
核心原则:UseCase只依赖Repository接口,不依赖具体实现。这样你的业务逻辑就和数据源解耦了。
9.2 单一职责:一个UseCase只做一件事
我在项目中遇到过这样的场景:一个同事写了个UserUseCase,里面包含了登录、注册、获取资料、修改密码、退出登录五个方法。看起来挺方便对吧?但问题来了——
- 测试的时候,你想单独测登录逻辑,得把整个类都mock了
- 修改注册逻辑时,可能不小心影响到登录
- 代码复用性差,别的模块想用登录功能,得依赖整个UserUseCase
所以我的建议是:一个UseCase只暴露一个公共方法。比如:
class LoginUseCase(
private val authRepository: AuthRepository
) {
suspend operator fun invoke(request: LoginRequest): Result<LoginResponse> {
return authRepository.login(request)
}
}
你看,这个UseCase只有一个invoke方法。调用方用起来特别清爽:
viewModelScope.launch {
val result = loginUseCase(LoginRequest("user", "pass"))
}
小技巧:用operator fun invoke可以让UseCase像函数一样调用,代码更简洁。我个人习惯所有UseCase都统一用这个命名。
9.3 UseCase的输入输出规范
嗯,这里要注意。很多初学者喜欢把UseCase的输入输出定义得很随意——输入直接传原始参数,输出直接返回数据实体。这样做的问题在于:
- 参数多了以后,方法签名变得很长
- 返回的数据可能包含调用方不需要的字段
- 难以扩展——比如登录时突然需要传个设备ID,你得改所有调用方
我建议的做法是:每个UseCase定义自己的输入和输出数据类。
输入规范
data class LoginRequest(
val username: String,
val password: String,
val deviceId: String? = null // 可选参数,不影响现有调用
)
输出规范
data class LoginResponse(
val token: String,
val userId: String,
val expiresAt: Long
)
这样做的优势很明显:
- 参数变化时,只需要改Request类,调用方不受影响
- 返回的数据是调用方真正需要的,不多不少
- 每个UseCase的输入输出自成一体,便于单元测试
避坑指南:我曾经犯过一个错误——把Repository返回的Entity直接作为UseCase的输出。后来UI层需要的数据变了,我不得不改Repository、改UseCase、改ViewModel,牵一发而动全身。所以记住:UseCase的输出应该是为调用方量身定制的。
9.4 UseCase的典型结构
一个标准的UseCase通常长这样:
class GetProductListUseCase(
private val productRepository: ProductRepository
) {
suspend operator fun invoke(
request: GetProductListRequest
): Result<GetProductListResponse> {
// 可以在这里加一些业务逻辑
// 比如缓存策略、数据转换、权限检查等
return productRepository.getProducts(
page = request.page,
size = request.size
).map { products ->
GetProductListResponse(
items = products.map { it.toProductItem() },
totalCount = products.size
)
}
}
}
data class GetProductListRequest(
val page: Int = 1,
val size: Int = 20
)
data class GetProductListResponse(
val items: List<ProductItem>,
val totalCount: Int
)
你看,这个UseCase做了三件事:
- 接收请求参数
- 调用Repository获取数据
- 把数据转换成调用方需要的格式
仅此而已。没有网络请求,没有数据库操作,没有UI更新——那些是其他层的事。
9.5 用SVG理清UseCase的定位
下面这张图展示了UseCase在Clean架构中的位置,以及它和上下游的关系:
从这张图可以清楚看到:UseCase处于中间层,它不直接操作数据源,也不直接更新UI。它的职责就是编排业务逻辑。
9.6 什么时候需要UseCase?
你可能会问:是不是每个操作都要写一个UseCase?我的答案是:看情况。
| 场景 | 是否需要UseCase | 原因 |
|---|---|---|
| 简单的数据获取(如获取用户信息) | 建议需要 | 方便后续加缓存、权限检查等逻辑 |
| 复杂的业务操作(如提交订单) | 必须需要 | 涉及多个Repository调用、数据校验、状态管理 |
| 纯UI操作(如点击按钮播放动画) | 不需要 | 这属于UI层职责,不需要业务逻辑 |
我的经验:刚开始写项目时,我倾向于为每个Repository方法都建一个UseCase。后来发现有些UseCase只是简单转发,反而增加了代码量。现在我遵循的原则是:如果这个操作未来可能增加业务逻辑,就建UseCase;如果确定只是简单转发,可以直接在ViewModel里调用Repository。但团队统一风格更重要,别一半用一半不用。
9.7 总结
UseCase层设计其实就三个要点:
- 单一职责:一个UseCase只暴露一个公共方法
- 输入输出规范:用Request/Response数据类封装
- 只依赖Repository接口:不依赖具体实现
做到这三点,你的业务逻辑就会变得清晰、可测试、易维护。说白了,UseCase就是你的业务逻辑的"守门员"——它确保所有业务操作都按照规范来,不会乱成一锅粥。
公众号:蓝海资料掘金营,微信deep3321