Android 架构七条铁律:架构师真正设计的不是代码,而是秩序

前段时间,和一个年轻开发者聊 Android 架构。

他谈起 MVVM。

谈起 MVI。

谈起 Clean Architecture。

也谈起各种设计模式。

他说了很多。

我听了很久。

然后问了一个问题。


"为什么 Repository 不应该随意在内部启动协程?"


他沉默了。


很多人学架构。

学的是工具。

学的是框架。

学的是分层。

学的是各种图。

但很少有人真正思考:

这些规则究竟是为了解决什么问题?


项目崩溃。

不是因为少了一张架构图。

而是因为责任不清。

任务失控。

依赖混乱。

状态失去了唯一来源。


而架构师真正要做的事情。

从来不是画图。

而是在混乱出现之前。

制定规则。

让每一种行为都有边界。

让每一份责任都有归属。


第一条铁律:Repository 必须 Main Safe

一个成熟的系统。

不应该把自己的问题交给别人。


如果调用 Repository 的时候。

还需要思考:

scss 复制代码
withContext(Dispatchers.IO)

应该放在哪里。

那么问题已经出现了。


调用方只应该关心业务。

而不是 Repository 内部如何执行。

例如:

kotlin 复制代码
suspend fun getUser(): User

无论内部使用:

  • Room
  • Retrofit
  • 文件读写
  • JNI
  • 其他阻塞式接口

Repository 都应该保证这个接口可以安全地从主线程调用。

例如:

kotlin 复制代码
class UserRepository(
    private val api: UserApi,
    private val ioDispatcher: CoroutineDispatcher
) {

    suspend fun getUser(): User =
        withContext(ioDispatcher) {
            api.getUser()
        }
}

当然,不是所有操作都应该机械地切换到 IO。

CPU 密集型任务应该考虑 Default。

本身已经具备主线程安全能力的挂起 API,也不需要重复切换线程。

真正重要的是:

Repository 对自己的执行策略负责,而不是把线程调度的负担交给调用方。


第二条铁律:Repository 不得随意启动脱离调用方的任务

很多线上问题。

都来自一句代码。

arduino 复制代码
scope.launch {
    // 执行业务任务
}

任务启动了。

却没人知道:

  • 谁负责结束?
  • 谁负责异常?
  • 谁负责等待结果?
  • 谁负责处理重复任务?

看起来很方便。

实际上已经脱离控制。


例如:

kotlin 复制代码
class UserRepository {

    fun refresh() {
        CoroutineScope(Dispatchers.IO).launch {
            // 刷新用户数据
        }
    }
}

调用方甚至不知道任务什么时候完成。

更不知道它什么时候结束。


所以规则很简单:

谁发起任务,谁拥有任务。

Repository 提供能力。

调用方决定任务生命周期。

kotlin 复制代码
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    fun refresh() {
        viewModelScope.launch {
            repository.refresh()
        }
    }
}

这里,任务属于 ViewModel 的生命周期。

ViewModel 销毁。

任务也会随之取消。

当然,应用级同步任务可以由应用级作用域管理。

但必须明确任务的所有者。

不要让任务成为一个无人管理的孤儿。


第三条铁律:状态必须有明确的唯一事实来源

混乱的开始。

往往来自多个真相。


Activity 改状态。

Fragment 改状态。

Service 改状态。

Repository 也维护一份状态。

最后。

每个人都说自己是正确的。

系统却不知道该相信谁。


这就是为什么我们需要:

Single Source Of Truth(SSOT)。

例如用户信息:

yaml 复制代码
             UI
              |
              v
          ViewModel
              |
              v
          Repository
              |
              v
             Room

Room 作为本地用户数据的唯一事实来源。

网络请求成功后。

Repository 更新 Room。

UI 通过 Flow 观察 Room。

数据发生变化。

UI 自动更新。


这里需要注意:

SSOT 并不是说整个应用只能有一个状态对象。

而是对于同一份业务数据,必须有明确、可信的事实来源。

一个事实,不应该有多个互相竞争的主人。


第四条铁律:一次性操作使用 suspend,持续数据使用 Flow

单次动作。

和持续变化。

本来就不是同一种东西。


例如登录:

kotlin 复制代码
suspend fun login(
    account: String,
    password: String
): User

它是一次操作。

调用一次。

等待一个结果。


观察用户信息:

kotlin 复制代码
fun observeUser(): Flow<User>

它是持续的数据流。

数据可能发生多次变化。

订阅者持续接收更新。


两者的语义完全不同。

如果所有异步操作都使用 Callback。

调用关系容易逐渐变得复杂。

如果所有操作都包装成 Flow。

又会让一次性操作失去原本简单的语义。

所以:

  • 一次性操作,使用 suspend。
  • 持续数据变化,使用 Flow。
  • 当前 UI 状态,通常使用 StateFlow。
  • 一次性 UI 效果,根据场景选择合适的事件传递机制。

不要为了统一 API。

而牺牲 API 本身的语义。

好的接口设计,不是形式统一,而是表达准确。


第五条铁律:禁止用全局事件总线隐藏业务依赖

最危险的依赖。

往往是看不见的依赖。


发送者不知道谁接收。

接收者不知道谁发送。

两者之间没有明确的关系。


刚开始很方便。

后来越来越混乱。

例如:

less 复制代码
FlowBus.post(UserLoginEvent())

另一个地方:

arduino 复制代码
FlowBus.subscribe<UserLoginEvent> {
    // 更新用户信息
}

代码看起来很简单。

但问题来了:

  • 谁发送了这个事件?
  • 谁订阅了这个事件?
  • 订阅者什么时候开始工作?
  • 订阅者什么时候结束?
  • 多个订阅者之间有没有顺序要求?

出了问题以后。

所有人都在问:

这个事件是谁发的?


事件机制本身没有错。

广播、系统通知、跨模块事件,都有合理的使用场景。

真正的问题是:

使用全局事件总线,隐藏原本应该明确的业务依赖关系。

能够通过明确的接口表达的依赖。

就不要随意改造成全局事件。

看不见的关系。

最难维护。


第六条铁律:异常必须有明确的处理责任

很多系统不是死于异常。

而是死于异常消失。


这样的代码很常见:

php 复制代码
try {
    repository.getUser()
} catch (e: Exception) {

}

什么都没有发生。

却什么都已经发生了。

调用方不知道失败。

日志里没有记录。

系统继续运行。

然后慢慢腐烂。


还有一种更隐蔽的问题。

在 Kotlin 协程中:

php 复制代码
try {
    repository.getUser()
} catch (e: Exception) {
    // 处理异常
}

这里可能连 CancellationException 一起捕获。

这会破坏协程的取消语义。

正确的处理方式:

php 复制代码
try {
    repository.getUser()
} catch (e: CancellationException) {
    throw e
} catch (e: Exception) {
    // 处理业务异常
}

不同层次应该承担不同的责任:

  • Data 层:处理底层异常,必要时转换成业务错误。
  • Domain 层:定义业务操作的失败语义。
  • UI 层:决定如何向用户展示错误。

异常可以被转换。

可以被处理。

但不能被随意隐藏。

错误不可怕,失去错误的可见性才可怕。


第七条铁律:异步等待优先使用挂起,而不是阻塞线程

很多人已经使用协程。

思维却还停留在线程时代。


他们仍然喜欢:

ini 复制代码
Future.get();

CountDownLatch.await();

BlockingQueue.take();

这些 API 都可能让线程陷入阻塞等待。


但协程提供了另一种选择。

kotlin 复制代码
suspend fun getUser(): User {
    return suspendCancellableCoroutine { continuation ->
        api.getUser { user ->
            continuation.resume(user)
        }
    }
}

等待结果的时候。

协程可以挂起。

执行线程不必一直被占用。

等结果到达。

协程再恢复执行。


这里必须区分两个概念:

方式 等待时发生什么
线程阻塞 线程被占用
协程挂起 协程暂停,线程可以执行其他任务

当然。

协程并不会自动把所有阻塞 API 变成非阻塞 API。

例如:

kotlin 复制代码
suspend fun getUser(): User =
    withContext(Dispatchers.IO) {
        blockingApi.getUser()
    }

这里仍然存在阻塞。

只是把阻塞操作隔离到了 IO Dispatcher。


协程真正的价值。

不是简单地切换线程。

而是:

在等待结果的时候,不必无谓地占用执行线程。

等待结果是业务需求。

阻塞线程只是实现方式之一。


尾声:架构师真正设计的是什么?

后来那个年轻人又来了。

我还是问他同一个问题。


"架构到底是什么?"


这次他没有提 MVVM。

没有提 MVI。

也没有提任何框架。


他说:


"架构是在项目越来越复杂的时候。"

"仍然让所有人知道。"

"数据从哪里来。"

"状态归谁管理。"

"任务谁负责。"

"异常谁处理。"

"对象活多久。"

"模块应该如何依赖。"


我点了点头。


因为直到这一刻。

他终于明白。


Clean Architecture 帮助我们建立代码边界。

工程实践中的架构规则。

帮助我们建立行为边界。

前者解决依赖关系。

后者解决责任归属。

两者共同决定系统能否长期保持稳定。


架构不是技术名词。

不是框架。

也不是一张漂亮的分层图。

架构是一套规则。

规则形成边界。

边界明确责任。

责任形成秩序。

而秩序。

才是大型系统能够长期存活的原因。


架构师真正设计的不是代码。

而是代码背后的秩序。

相关推荐
xiangxiongfly9153 小时前
Android GsonFactory总结
android·gson·gsonfactory
WythePlus4 小时前
Android 14 SELinux策略深度解析
android·安全策略·selinux策略
俩个逗号。。8 小时前
Kotlin flow 详解
android·开发语言·kotlin
橙光成长10 小时前
android-ad-cleaner:android免 Root 批量关闭系统开屏与营销推送
android·1024程序员节
深圳雨林凯AI10 小时前
图案裂变的底色衰减:alpha通道、亮度对比与白墨分色的量化拆解
android
mmsx10 小时前
Android 模块化收口:能力组合框架、手写 LifecycleOwner 与热修复共存
android
恋猫de小郭13 小时前
Flutter 可以更直接地调用原生 API 了,Dart Direct Native Interop
android·前端·flutter
2501_9160088913 小时前
iPhone 设备信息查看,用 KeyMob 看型号、序列号、电池健康和 UDID
android·ios·小程序·https·uni-app·iphone·webview
好的佩奇1 天前
蓝牙之通信架构
android·flutter·蓝牙