谁最了解复杂度,谁最该承担它。
前言
在做 Android 架构评审时,我经常看到这样的代码:
ViewModel 在关心什么?它在关心缓存策略、缓存 key 的格式、是否要强制刷新。这些本不该是它的事。
这就是复杂度泄露——一层不该承担的复杂度,流窜到了它的调用方。
这篇文章想聊一个设计原则,以及它在 Android Data 层的落地实践。
一、核心原则:复杂度要被安置到合适的地方
先说原则的本质:
不要让调用方承担你本该承担的复杂度,谁最了解复杂度,谁最该承担它。
这句话背后是封装的哲学。但封装不是把复杂度藏起来,而是把它安置到最合适的地方。
在 Android Data 层,三类角色各司其职:
| 角色 | 了解什么 | 该承担什么 |
|---|---|---|
| RemoteDataSource | 网络协议、序列化、HTTP 状态码 | 网络重试、超时处理、异常转换 |
| LocalDataSource | 数据库结构、SQL、缓存格式 | 读写操作、数据版本迁移 |
| Repository | 数据来源、缓存策略、刷新时机 | 数据流编排、来源判断、异常统一封装 |
| ViewModel | 用户意图、UI 状态 | 只管"我要什么数据" |
每一层只承担属于自己的那一份复杂度,不多也不少。
二、复杂度泄露的三种常见表现
1. 接口参数暴露实现细节
forceRefresh、cacheKey、fallbackToLocal 这些参数,是 Repository 自己的内部决策,不应该暴露给 ViewModel。你把参数暴露出去的那一刻,就是在告诉调用方:你来决定怎么取数据吧。
2. 异常直接透传给上层
ViewModel 根本不该知道 HttpException 和 SQLiteException 是什么。这些是底层细节,Repository 应该把它们消化掉,转换成业务语义:
3. 数据来源判断逻辑外泄
这段逻辑本属于 Repository,写在 ViewModel 里是严重的职责错位。Repository 的核心价值之一,就是对上层屏蔽数据来源:
三、Repository 的边界:不是上帝类
这个原则有一个容易踩的坑——把"谁了解谁承担"理解成:
"那我把所有复杂度都塞进 Repository 就好了。"
然后 Repository 变成上帝类,几千行,什么都管。
封装 ≠ 藏污纳垢。
Repository 应该承担的是编排逻辑,不是实现细节。以下复杂度不该由 Repository 承担:
- 具体的 HTTP 请求构造 → 交给
RemoteDataSource - 具体的 SQL 语句 → 交给
LocalDataSource / DAO - 业务规则的组合计算 → 交给
UseCase
每一层向下调用时,都在说:"这部分复杂度我不了解,交给你。"
四、线程切换:被忽视的复杂度泄露
线程管理,是复杂度泄露里最隐蔽的一种——因为它不体现在接口参数里,而是藏在调用方式上。
反模式:调用方来切线程
表面上看没问题,但这背后的含义是:ViewModel 知道 getUser 是一个 IO 操作。
它为什么知道?因为 Repository 没有封装好线程,调用方被迫承担了"该在哪个线程跑"这个复杂度。
这是一种隐式的实现细节泄露。
正确做法:DataSource 自己承担线程切换
谁最了解一个操作该在哪个线程跑?DataSource 自己。
网络请求跑在 IO 线程,数据库读写跑在 IO 线程,这是 DataSource 的内部知识,不该往外透。
suspend 函数版:
这样 Repository 和 ViewModel 调用时,完全不需要关心线程:
Flow 版:用 flowOn 而非在 collect 侧切换
当 DataSource 返回 Flow 时,线程控制用 flowOn,它影响的是上游的执行线程,语义更精确:
flowOn 应该在最了解自己执行代价的那一层声明,通常是 DataSource,而不是调用方。
特别说明:Retrofit 和 Room 已经帮你做了
如果你用的是:
- Retrofit + 协程:
suspend fun接口方法内部已经自动切到后台线程,不需要再加withContext - Room + 协程/Flow:Room 的
suspend查询和Flow返回值,也已经在内部处理了线程
这种情况下,DataSource 层不需要额外的 withContext 或 flowOn。
但如果你有自定义 DataSource(比如读文件、操作 SharedPreferences、调用第三方同步 SDK),就必须自己在 DataSource 内部处理线程切换,绝对不能把这个负担留给调用方。
把线程也纳入职责表
回到开头的那张职责表,线程管理同样是复杂度的一部分:
| 角色 | 了解什么 | 该承担什么 |
|---|---|---|
| RemoteDataSource | 网络 IO、协议细节 | 网络重试、异常转换、线程切换 |
| LocalDataSource | 数据库 IO、文件 IO | 读写操作、数据迁移、线程切换 |
| Repository | 数据来源、缓存策略 | 数据流编排、来源判断、异常封装 |
| ViewModel | 用户意图、UI 状态 | 只管"我要什么数据",无需感知线程 |
ViewModel 里最理想的状态:launch 后面不跟任何 Dispatcher。
五、实践中的细节
用 sealed class 表达业务语义,而非原始异常
这样 ViewModel 处理的是业务错误,而不是底层技术细节。
Flow 是天然的复杂度封装器
调用方拿到的只是一个 Flow,完全不知道里面发生了什么。
不要为了"灵活性"过早暴露参数
很多时候我们加 forceRefresh: Boolean 是因为"万一 ViewModel 需要手动刷新呢"。
更好的方案是:
两个函数,语义清晰,调用方不需要理解 forceRefresh=true 到底意味着什么。
六、一句话总结
复杂度不会消失,只会转移。好的设计,是让它停留在最了解它的那一层,既不向上泄露,也不堆积。