模块化设计与分层架构

做 SDK 集成架构,最怕什么?我告诉你,最怕「牵一发而动全身」。几年前我接手过一个支付 SDK,业务逻辑、网络请求、UI 渲染全写在一个模块里。结果呢?改个 UI 样式,支付回调就挂了。改个网络超时时间,整个 app 启动黑屏。那叫一个惨。

后来我学乖了。模块化 + 分层,才是 SDK 抗揍的关键。说白了,就是把一个庞然大物,拆成若干个互不干扰的小积木。每个积木只管自己的事,出了问题,我只换那一块,不用把整个房子拆了。

三层架构:我给 SDK 划的「地界」

我个人习惯,把 SDK 分成三层。你想想看,一栋楼总得有个地基,有个管道层,还有个样板间对吧?SDK 也一样。

层级 角色 核心职责 我踩过的坑
业务层 样板间 面向使用者的 API、界面、状态管理 有一次把支付页面动画加在业务层,结果底层换引擎,页面崩了
核心层 管道层 协议封装、数据流、权限校验、事件总线 事件总线之前不独立,业务层一改监听就炸
公共层 地基 网络、存储、日志、日志上报、基础工具 网络库从 OkHttp 换到 Retrofit,整个 SDK 几乎重写

嗯,这里要注意:业务层绝不能直接调用公共层。我见过最离谱的代码,是支付页面里直接写了 HttpURLConnection。一旦网络底层想升级,得把支付页面翻个底朝天。中间必须隔一个核心层,让核心层去调度公共层的能力。

核心原则:上层可以依赖下层,下层不能依赖上层。这叫单向依赖。不遵守?后面重构的时候,你会想把电脑摔了。

模块划分:怎么切才不后悔

模块划分,我有一套「三看」口诀:看职责、看变化频率、看复用性。

  • 看职责:一个模块只做一件事。比如「登录模块」就管登录,别把「个人资料页」也塞进去。
  • 看变化频率:经常变的逻辑,独立出来。比如 UI 样式、文案。底层公共能力,如日志、加密,几乎不变,放公共层。
  • 看复用性:如果好几个业务模块都需要「图片选择」功能,那就抽成独立模块,别每个业务模块自己重复造轮子。

我在项目中遇到过,有个团队把「分享」和「统计」写在一个模块里。原因?当时觉得分享完顺便上报统计,省事。结果统计上报规则一变,分享功能也得跟着发布。你想想看,是不是很亏?

数据流模型:一张图看懂哪层该干嘛

SDK 模块化分层数据流模型 业务层 支付页面 · 登录弹窗 · 分享菜单 状态管理 · UI 样式 · 用户交互 调用核心层 核心层 接口定义 · 事件总线 · 协议封装 权限校验 · 数据流调度 · 可扩展点 调用公共层 公共层 网络请求 · 本地存储 · 日志上报 加密解密 · 基础工具类 数据流方向与依赖说明 用户操作 → 业务层 业务层 → 核心层(接口调用) 核心层 → 公共层(能力调用) 禁止:业务层直接调公共层 模块间通信走接口 不直接依赖具体实现 每个模块独立发布 改动只影响自身版本 核心层提供 SPI 扩展点

这张图是我提炼了很多项目后的通用模型。你看箭头方向,数据永远是从上往下流动。业务层说「我要登录」,核心层去调公共层的网络请求,公共层只管把数据拿回来。各层不越界,出了问题我能一眼定位。

模块间通信:别让它们直接「聊」

模块之间怎么传消息?直接 import 对方类?那是耦合的开端。我建议用 接口 + 事件总线 的方式。

// 公共层定义接口
public interface ILogger {
    void log(String tag, String message);
}

// 核心层注册使用
public class EventBus {
    private ILogger logger;
    
    public void setLogger(ILogger logger) {
        this.logger = logger;
    }
    
    public void post(String event) {
        logger.log("EventBus", event);
        // 通知订阅者...
    }
}

// 业务层在初始化时注入
public class PaymentModule {
    public void init(ILogger logger) {
        EventBus bus = new EventBus();
        bus.setLogger(logger);  // 依赖注入,不直接 new 实现类
    }
}

你看,业务层只认识 ILogger,根本不关心底层是写到控制台还是上传到服务器。想换实现?换个对象注入就行了。这样做的好处,我在项目里体会很深——有一次需要把日志从本地文件改成云端上报,只改了一个注入点,其他代码一行没动。

技巧:模块间接口定义可以做成 SPI(Service Provider Interface)形式。核心层提前预留扩展点,业务层通过配置文件或注解加载实现。这样新增模块不用改核心层代码,类似于插件系统。

依赖方向怎么定?我的决策清单

有学员问过我:我到底该把某个功能放哪一层?只看一条:这个功能是否会被多个业务模块共用?

  • 是 → 放公共层。例如网络请求、加密、存储。
  • 否,但会被核心层调度 → 放核心层。例如权限校验、协议解析。
  • 只给某个业务用,且变化频繁 → 放业务层。例如支付页面动画、登录页的 UI 交互。

我曾经遇到过,有个团队把「图片压缩」功能放在了一个业务模块里。结果三个模块都需要压缩图片,只能 copy 三份代码。你想想看,维护成本翻了三倍。后来统一提到公共层,改动一次,全局生效。

警告:别搞「循环依赖」。我有次接手一个老项目,A 模块依赖 B,B 又依赖 A。编译能过,但启动时初始化顺序就乱套了,有时能跑,有时直接闪退。解决方式?引入一个中介接口层,让 A 和 B 都依赖接口,不直接依赖实现类。

设计规范:提前定好,后面少吵架

模块化不是光分文件就行。团队里每个人理解不同,最后可能又变成一锅粥。我习惯在项目开始时定几个硬性规范:

规范项 具体规则 为什么
包命名 com.公司.sdk.层名.模块名 一看包名就知道该放哪层,新人不容易放错
模块可见性 向上层暴露接口,不暴露内部类 别人改了你的内部实现,你可能还不知道
初始化顺序 公共层 → 核心层 → 业务层 下层没有准备好,上层调用必挂
删除一个类 必须先确认没有上层引用 我删过一个工具类,结果三个业务模块同时编译失败

嗯,还有一条:每个模块独立版本号。刚开始我觉得麻烦,但后来发现,如果整个 SDK 一个版本号,改一个 UI 样式,所有模块都得重新发布。拆开版本后,我只升那个模块的版本,引用它的业务模块自己决定要不要升。灵活得多。

验收标准:怎么知道你分对了?

分完之后,总要有个检验方法吧?我自己的标准是:

  1. 单模块可独立测试:比如写个 Demo,只集成「登录模块」,不集成「支付模块」,能正常编译运行。
  2. 替换一个实现不伤其他:比如把日志库从 Log4j 换成 SLF4J,只动公共层,业务层代码不用改。
  3. 改动频率隔离:统计一个月内,各模块的修改次数。如果业务层改 10 次,公共层改 8 次,那说明分层有问题。公共层应该稳如泰山。
  4. 新人上手能快速定位:我让新来的同事看 10 分钟代码,然后问他:如果支付按钮点击后没反应,你可能去哪里排查?如果能说「先看业务层,再检查核心层的事件注册」,就说明分层清晰。

说白了,模块化和分层不是炫技。它的核心价值就一个:降低认知负担和变更成本。你在写每一行代码之前,先问自己:这行代码属于哪一层?它不应该知道我上下层是谁。做到这一点,你的 SDK 架构就有救了。