第二章:SDK架构设计——模块化、API规范与版本管理
架构设计这件事,说白了就是决定SDK的“骨架”该怎么长。骨架长歪了,后面再怎么贴肉都别扭。我接手过不少遗留SDK,那个模块之间就像一锅粥,改一个功能要牵连七八个文件。后来我总结出了一套经验,今天跟你聊聊。
2.1 模块化设计:让SDK能拆能合
模块化设计的核心就一句话:高内聚,低耦合。每个模块只做一件事,并且把这件事做好。
我个人习惯把SDK拆成这几层:
| 层级 | 职责 | 举例 |
|---|---|---|
| 核心层(Core) | 提供基础能力,数据模型、工具函数 | 网络请求封装、日志工具、线程管理 |
| 业务层(Business) | 实现具体业务逻辑 | 支付流程、登录注册、数据上报 |
| 接口层(API) | 对外暴露的门面,隔离内部变化 | SDK入口类、回调接口、配置类 |
| 平台适配层(Platform) | 处理各平台差异 | iOS/Android/Web的API桥接 |
举个例子,我在做支付SDK时,把“网络请求”放在核心层,“订单创建”放在业务层,“支付唤起”放在平台适配层。这有什么好处呢?后来接入微信支付时,我只改了平台适配层和业务层的几个接口,核心层没动——测试覆盖还是绿绿的。
- 接口隔离:模块之间只通过接口通信,不直接依赖实现类
- 依赖反转:高层模块不依赖低层模块,都依赖抽象
- 可插拔:模块可以独立加载、替换、卸载,互不影响
模块化还有一个容易被忽视的点——粒度控制。拆得太细,光接口定义就够你喝一壶;拆得太粗,又退回了“一锅粥”。我个人经验:一个模块的代码量控制在500~1000行比较合适,超出就考虑继续拆分。
我采用的SDK架构示意如下,它体现了核心模块之间的分层与协作关系:
2.2 API接口规范:设计一门“好用的语言”
API接口就是SDK跟外部世界沟通的语言。这门语言好不好,直接影响开发者上手的体验。
我从命名、参数设计、错误码三个方面来讲。
2.2.1 命名规则
- 动词+名词:用
createOrder()而不是order(),清晰表达行为 - 保持一致的语序:要么都是
getUserInfo()、setUserInfo(),不要混合fetchUser() - 避免缩写:
initialize()比init()更清晰,除非是行业通用缩写如URL
我记得有个合作伙伴的SDK,里面既有getUser()又有fetchProfile()还有loadMemberData()——三个意思差不多的接口,风格却完全不同。调用方得翻文档猜哪个是对的,体验很差。
2.2.2 参数设计
| 参数类型 | 推荐做法 | 不推荐做法 |
|---|---|---|
| 必选参数 | 放在方法签名前面 | 跟可选参数混在字典里 |
| 可选参数 | 使用对象/结构体封装 | 写十几个同名重载方法 |
| 回调参数 | 提供成功/失败两个闭包 | 用一个回调传错误码和结果 |
| 默认值 | 文档标注,且行为合理 | 默认为null引起空指针 |
Object或Map<String, Object>。我见过一个SDK的初始化方法就靠一个JSON传所有配置,IDE没有代码提示,写错字段名就要调试半天。
2.2.3 错误码体系
错误码要遵循“可读、可定位、可扩展”的原则。我用的是三层结构:
- 大类码:标识错误来源,如
NETWORK_ERROR、AUTH_ERROR - 细分码:标识具体原因,如
NETWORK_TIMEOUT、_EXPIRED - 错误信息:人类可读的描述,支持国际化
// 好的错误码设计
public enum SDKError {
NETWORK_TIMEOUT("NETWORK_ERROR", "1001", "网络连接超时,请检查网络"),
_EXPIRED("AUTH_ERROR", "2001", "登录凭证已过期,请重新登录"),
INVALID_PARAM("INPUT_ERROR", "3001", "参数[%s]格式不正确,请检查");
}
2.3 版本管理:如何优雅地“向前走”
版本管理做不好,SDK的接入方会非常痛苦——你可能在修复bug,对方却以为是破坏性变更。
我个人推荐使用语义化版本(SemVer):
主版本.次版本.补丁版本
- 主版本变更:不兼容的API改动。比如删了一个公开方法、改了参数签名
- 次版本变更:向下兼容的功能新增。比如加了一个新接口,老接口仍可用
- 补丁版本变更:向下兼容的bug修复。比如修正了某个边界情况
兼容性变更的判断其实有章可循:
| 变更类型 | 是否兼容 | 版本号变化 |
|---|---|---|
| 新增一个接口 | ✅ 兼容 | 次版本+1 |
| 修改接口参数类型 | ❌ 不兼容 | 主版本+1 |
| 修复内部bug | ✅ 兼容 | 补丁+1 |
| 废弃一个接口(保留实现) | ✅ 兼容但标记@Deprecated | 次版本+1 |
| 删除一个废弃接口 | ❌ 不兼容(建议先废弃一个主版本) | 主版本+1 |
我曾经在一个线上事故中学到:不要轻易改接口的默认行为。当时我把一个方法的超时时间从30秒改成15秒,觉得“优化性能嘛”。结果有调用方依赖这个超时来做业务逻辑,直接就崩了。从那以后,涉及默认值的修改,我都会走完整的废弃流程。
- 每次发版前,用自动化工具检查API兼容性(比如japicmp、clirr)
- 每个版本都要有CHANGELOG,写明变更内容、迁移指南、已知问题
- 旧版本至少要维护一个补丁版本周期,预留升级窗口
2.4 本章核心要点
- 模块化设计要分层,每层职责单一,接口清晰
- API设计要一致、可读、可定位错误,避免“猜谜式”调用
- 版本管理严格执行语义化,不搞“小修小补改大版”
这三件事做好,你的SDK就像一辆底盘扎实的车——开起来顺,修起来也简单。至于怎么让这辆车跑得更快、更稳,那是后面章节要聊的事。