4. 分层架构设计:表现层、业务层、数据层
咱们做 SDK 接入设计,最怕什么?怕改需求。
产品经理过来说「咱们这个支付按钮,从圆角改成直角吧」,你以为是改个样式,结果发现支付逻辑也绑在里面。等改完了,测试又发现「确认订单」页面也跟着变了——这就是典型的代码耦合。
分层架构就是为了解决这个问题。说白了,就是把不同职责的代码拆分到独立的层里,各层之间通过接口通信,互不干扰。我做了这么多年 SDK,几乎每个成型的产品最后都会走向分层。不是因为它时髦,而是它真的能救人。
4.1 为什么必须是三层?
三层架构是经过实战检验的最小合理拆分方案。每一层都有自己的明确边界:
- 表现层:负责与用户交互。展示界面、收集输入、响应事件。
- 业务层:核心逻辑所在。处理数据、执行策略、控制流程。
- 数据层:跟存储和网络打交道。读写本地文件、发起 API 请求、缓存管理。
为什么会是三层?你想想看,一个典型的 SDK 场景:用户点击了「分享」按钮。表现层响应用户点击,把事件交给业务层。业务层决定用什么渠道、组装什么内容,然后再告诉数据层去调微信的 API。数据层把结果返回业务层,业务层再把反馈传回表现层——这个闭环,天然就是三层。
个人习惯:我一般会在项目刚开始写第一行代码之前,先把三层目录结构建好。哪怕当前只有三个文件,这个架子也得支棱起来。后面每加一个功能,都知道该放哪。
注意:别为了分层而分层。如果你的 SDK 就三个文件,每层只包一个方法,那叫自欺欺人。分层有成本——接口定义、数据模型转换、对象传递——都是开销。等业务复杂度上来了再拆,完全不丢人。
4.2 表现层:把「怎么画」和「做什么」分开
表现层最容易被写烂。我见过太多开发者在 View 里直接写网络请求,「点击按钮 → 调接口 → 弹 Toast」,一气呵成。看起来挺紧凑,但等到要换 UI 组件库的时候就傻眼了——全得重写。
正确的做法是:表现层只关心界面的展示逻辑,不关心数据从哪来、怎么处理。它拿到什么数据就画什么,用户做什么操作就通知业务层。
我举个例子:一个登录模块。表现层拿到用户名和密码,点击登录,它只做一件事——把参数传给业务层的 login() 方法。然后等着业务层告诉它结果,显示成功或失败。至于密码要不要加密、要不要先检查网络、失败后要不要重试——这些全都在业务层处理。
// 表现层代码示意
fun onLoginButtonClicked() {
val username = usernameInput.text.toString()
val password = passwordInput.text.toString()
// 只调用业务层,不处理逻辑
viewModel.login(username, password)
}
关键原则:表现层不允许直接访问数据层。数据层只对业务层暴露接口。如果你在 View 里看到 database.query() 或 httpClient.get(),那就是越界了。
还有一个细节:表现层应该只持有轻量状态。不要挂载大量的业务对象。UI 组件是有生命周期的,页面跳来跳去,业务数据很容易丢失。我建议在表现层用简单的 ViewModel 或 Presenter,把复杂状态下沉到业务层。
4.3 业务层:SDK 的「大脑」
业务层是整个架构的核心。所有的决策、校验、路由、调度都在这里完成。它不关心数据怎么存、界面怎么画,它只关心「这个流程该怎么走」。
我记得有一次写一个推送注册的 SDK。表现层拿到设备 后,要判断当前用户是否登录、是否同意隐私协议、之前是不是注册过。如果都通过了,再把 发给后端。这些逻辑放哪?表现层?太肥。数据层?不合适。必须放在业务层。
业务层的代码通常长这样:
// 业务层代码示意
fun registerPush(: String): PushResult {
if (!checkUserLogin()) return PushResult.NotLogin
if (!checkPrivacyAgreed()) return PushResult.PrivacyNotAgreed
if (checkAlreadyRegistered()) return PushResult.AlreadyRegistered
return dataLayer.uploadPushToken()
}
你看,业务层用到了数据层(uploadPushToken),也用到了自己的子模块(检查登录、检查协议)。但它完全不知道外面是 Activity 还是 Fragment,也不知道数据是存在 SQLite 还是 Room 里。
我曾经犯过的错:在业务层里传了 Context。以为方便,结果单元测试跑不起来——测试环境没有 Android Context。后来我硬是用接口抽了一层,把所有 Android 依赖都剥离掉,业务层变成了纯 Kotlin,测试瞬间就通了。
4.4 数据层:把「存和取」封装好
数据层的职责很简单:提供数据,存回数据。但它踩的坑一点也不少。
首先,数据层要屏蔽数据来源。调用方不需要知道数据是来自本地缓存还是远程服务器。所以我会在数据层做一个统一的数据源抽象:本地数据源和远程数据源。业务层问数据层要数据,数据层自己决定先去缓存找,没有就去网络拉,然后缓存下来再返回。
其次,数据层要处理好线程问题。别忘了,网络请求和文件读写都是耗时的,不能卡主线程。我建议数据层内部默认异步,返回的结果用回调或协程包装好,业务层拿到的已经是线程安全的数据。
// 数据层结构示意
class UserDataRepository(private val local: LocalDataSource,
private val remote: RemoteDataSource) {
suspend fun getUser(): User {
val cached = local.getUser()
if (cached != null) return cached
val fresh = remote.fetchUser()
local.saveUser(fresh)
return fresh
}
}
一个小技巧:数据层的接口定义应该尽量接近业务模型,而不是数据库表结构。业务层拿到的 User 对象,是业务模型;数据层内部存的是 UserEntity,是数据模型。我建议在数据层内部完成转换,不要把 Entity 暴露出去。
4.5 三层之间的协作关系
现在我们把三层放到一起看,它们长这样:
这张图说的是什么?每一层只能跟相邻层通信。表现层不能直接跳到数据层。业务层不能越过数据层直接写文件。这个规则一旦打破,你的代码就会慢慢纠缠在一起。
4.6 实践中的边界检查清单
代码写完了,怎么检查有没有越界?我这里有一份自己常用的清单:
| 检查项 | 合格标准 |
|---|---|
| 表现层是否包含网络请求 | 禁止 |
| 表现层是否包含数据库操作 | 禁止 |
| 业务层是否引用 UI 组件 | 禁止 |
| 业务层是否持有 Context | 建议否,必须时用接口抽象 |
| 数据层是否抛出业务异常 | 禁止,应转换为业务层能理解的错误模型 |
| 各层接口是否明确 | 必须,接口变化要评估上下游影响 |
这份清单我每次做代码审查都会用。特别是新人写的 SDK,经常一不小心就在 Activity 里调数据库。发现了就改,改了再记,慢慢就养成习惯了。
核心总结:分层架构不是为了好看,是为了让 SDK 经得起变化。表现层换了,业务层不动;数据源换了,表现层不知情。每一层只管好自己的事,其他的交给接口。你在项目里写三行代码前就定好这个规矩,后面会轻松非常多。