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 三层之间的协作关系

现在我们把三层放到一起看,它们长这样:

SDK 三层架构协作关系图 表现层 UI 渲染 事件处理 状态绑定 View 生命周期 业务层 核心流程编排 权限校验 策略执行 状态管理 数据层 缓存管理 模型转换 网络请求 本地存储 通信规则 • 表现层只能调用业务层 • 业务层只能调用数据层 • 不允许跨层调用 • 数据通过接口契约传递

这张图说的是什么?每一层只能跟相邻层通信。表现层不能直接跳到数据层。业务层不能越过数据层直接写文件。这个规则一旦打破,你的代码就会慢慢纠缠在一起。


4.6 实践中的边界检查清单

代码写完了,怎么检查有没有越界?我这里有一份自己常用的清单:

检查项 合格标准
表现层是否包含网络请求 禁止
表现层是否包含数据库操作 禁止
业务层是否引用 UI 组件 禁止
业务层是否持有 Context 建议否,必须时用接口抽象
数据层是否抛出业务异常 禁止,应转换为业务层能理解的错误模型
各层接口是否明确 必须,接口变化要评估上下游影响

这份清单我每次做代码审查都会用。特别是新人写的 SDK,经常一不小心就在 Activity 里调数据库。发现了就改,改了再记,慢慢就养成习惯了。

核心总结:分层架构不是为了好看,是为了让 SDK 经得起变化。表现层换了,业务层不动;数据源换了,表现层不知情。每一层只管好自己的事,其他的交给接口。你在项目里写三行代码前就定好这个规矩,后面会轻松非常多。