第二章:SDK架构设计——模块化、API规范与版本管理

架构设计这件事,说白了就是决定SDK的“骨架”该怎么长。骨架长歪了,后面再怎么贴肉都别扭。我接手过不少遗留SDK,那个模块之间就像一锅粥,改一个功能要牵连七八个文件。后来我总结出了一套经验,今天跟你聊聊。

2.1 模块化设计:让SDK能拆能合

模块化设计的核心就一句话:高内聚,低耦合。每个模块只做一件事,并且把这件事做好。

我个人习惯把SDK拆成这几层:

层级 职责 举例
核心层(Core) 提供基础能力,数据模型、工具函数 网络请求封装、日志工具、线程管理
业务层(Business) 实现具体业务逻辑 支付流程、登录注册、数据上报
接口层(API) 对外暴露的门面,隔离内部变化 SDK入口类、回调接口、配置类
平台适配层(Platform) 处理各平台差异 iOS/Android/Web的API桥接

举个例子,我在做支付SDK时,把“网络请求”放在核心层,“订单创建”放在业务层,“支付唤起”放在平台适配层。这有什么好处呢?后来接入微信支付时,我只改了平台适配层和业务层的几个接口,核心层没动——测试覆盖还是绿绿的。

模块化的关键原则:
  • 接口隔离:模块之间只通过接口通信,不直接依赖实现类
  • 依赖反转:高层模块不依赖低层模块,都依赖抽象
  • 可插拔:模块可以独立加载、替换、卸载,互不影响

模块化还有一个容易被忽视的点——粒度控制。拆得太细,光接口定义就够你喝一壶;拆得太粗,又退回了“一锅粥”。我个人经验:一个模块的代码量控制在500~1000行比较合适,超出就考虑继续拆分。

我采用的SDK架构示意如下,它体现了核心模块之间的分层与协作关系:

SDK模块化架构图 SDK 模块化架构 网络请求模块 日志工具模块 数据加密模块 —— 核心层 —— 用户认证业务 支付核心业务 数据上报业务 —— 业务层 —— SDK入口类 回调接口 —— 接口层 —— 平台适配层 (iOS / Android / Web 桥接)
避坑指南:模块之间不要共享全局状态。我以前把用户登录放在一个静态变量里,结果多个模块同时读写,线上事故出了两回。后来改成通过接口注入的方式,干净多了。

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秒,觉得“优化性能嘛”。结果有调用方依赖这个超时来做业务逻辑,直接就崩了。从那以后,涉及默认值的修改,我都会走完整的废弃流程。

版本管理的三条纪律:
  1. 每次发版前,用自动化工具检查API兼容性(比如japicmp、clirr)
  2. 每个版本都要有CHANGELOG,写明变更内容、迁移指南、已知问题
  3. 旧版本至少要维护一个补丁版本周期,预留升级窗口

2.4 本章核心要点

  • 模块化设计要分层,每层职责单一,接口清晰
  • API设计要一致、可读、可定位错误,避免“猜谜式”调用
  • 版本管理严格执行语义化,不搞“小修小补改大版”

这三件事做好,你的SDK就像一辆底盘扎实的车——开起来顺,修起来也简单。至于怎么让这辆车跑得更快、更稳,那是后面章节要聊的事。