4、破坏性变更的识别:接口签名变化、行为变化、删除废弃API
做SDK兼容测试,说白了就是在跟“变化”打交道。我个人习惯把破坏性变更分成三类:接口签名变化、行为变化、删除废弃API。这三兄弟,每一个都能让你的调用方在升级时“炸”得措手不及。
我记得刚带团队那会儿,有个小伙子升级了一个底层库,只改了方法名里的一个单词。结果呢?全公司几百个调用方,编译挂了整整一上午。嗯,从那以后,我们就定了个规矩:每次发版前,必须过一遍这三类变更。
4.1 接口签名变化:最直接的“杀手”
接口签名变化,就是你改了方法的名字、参数或者返回值。这种变更,编译期就能检测到,算是最“友好”的破坏性变更了。
4.1.1 名字重命名
你想想看,用户之前写的是 oldMethod(),你发新版给改成了 newMethod()。用户代码直接报错。这就是典型的破坏性变更。
核心原则:不要重命名公开API。如果非要改,必须走废弃流程,给用户一个迁移周期。
4.1.2 参数变化
参数变化有三种情况:增加参数、删除参数、修改参数类型。
| 变更类型 | 兼容性影响 | 示例 |
|---|---|---|
| 增加必选参数 | 破坏性 | void foo(int a) → void foo(int a, int b) |
| 删除参数 | 破坏性 | void foo(int a, int b) → void foo(int a) |
| 修改参数类型 | 破坏性 | void foo(int a) → void foo(long a) |
| 增加可选参数 | 可兼容(需有默认值) | void foo(int a) → void foo(int a, int b = 0) |
我的建议:在Java/Kotlin中,可以考虑用@NonNull和@Nullable注解来明确参数约束。我在项目中遇到过,只改了个参数注解,结果用户的空指针测试全红了。
4.1.3 返回值变化
返回值的变化容易被忽略。比如你把返回值从 List 改成了 Set,虽然编译可能通过,但用户的迭代逻辑可能完全不一样了。
举个简单的例子:
// 旧版本
public List<String> getNames() { ... }
// 新版本
public Set<String> getNames() { ... }
用户的代码如果依赖了 List 的 get(index) 方法,就直接炸了。这就是典型的破坏性变更。
4.2 行为变化:最隐蔽的“地雷”
行为变化是最坑的。编译期完全检查不到,只有运行起来才会暴露。说白了,你改了方法的内部逻辑,但没改签名。
我曾经遇到过一个案例:一个日志库的 setLevel() 方法,老版本是 setLevel(int),传入 0 表示关闭日志。新版改成 0 表示DEBUG级别,关闭日志要用 -1。编译全过,但用户的线上日志量暴涨了100倍。嗯,这就是行为变化的典型恶果。
4.2.1 阈值/边界值变化
最常见的坑。比如缓存大小、超时时间、重试次数这些配置。
- 旧版本:最大缓存条目 = 1000
- 新版本:最大缓存条目 = 500
用户的业务如果依赖了大缓存,性能可能直接下降。这种变更,必须写在Release Notes里。
4.2.2 异常/错误处理变化
老版本抛 IOException,新版本改抛 RuntimeException。用户的try-catch直接就废了。
避坑指南:我曾经在升级SDK时,把受检异常改成了非受检异常。结果用户代码里的try-catch块全部失效,线上出现了大量未捕获异常。从那以后,我坚持:异常体系变更,一定要跟用户提前沟通。
4.2.3 并发/线程模型变化
这是最难排查的行为变化。比如你把一个方法从同步改成了异步,或者改了锁的粒度。
// 旧版本:同步执行
public Result executeSync() { ... }
// 新版本:异步执行
public void executeSync() {
new Thread(() -> { ... }).start();
}
用户的代码如果依赖了方法执行完成后的结果,那直接就挂了。这种变更,说到底是设计层面的问题。
4.3 删除废弃API:最后的“告别”
删除API是破坏性变更里最“决绝”的一种。一旦你删了,用户的旧代码就永远跑不起来了。
我建议的流程是:标记废弃 → 保留一个版本 → 确认无使用 → 删除。
4.3.1 标记废弃的标准做法
- 在方法/类上添加
@Deprecated注解(Java)或@deprecatedJSDoc(JavaScript) - 在文档中说明替代方案
- 在Release Notes中明确废弃时间线
4.3.2 删除前的确认清单
- 检查内部调用:SDK自身是否还有引用?
- 检查外部使用:是否有已知的调用方?
- 确认迁移方案:替代API是否已经稳定?
- 设置废弃期限:至少保留一个大版本周期的兼容。
我的经验:在实际项目中,我会在废弃方法内部加一个日志打印,记录调用方的堆栈。这样可以在删除前精确统计,到底还有多少人在用。嗯,这个技巧很实用。
4.4 三类破坏性变更的关系图
下面这张图,我整理了这三类变更的核心关系。你可以把它当作一个快速检查清单。
你想想看,如果你的SDK同时发生了这三种变更,用户的升级成本会有多高?所以我一直强调:破坏性变更的识别,不是等到发版前才做的,而是在设计阶段就要开始考虑的。
个人习惯:我每次写新API时,都会问自己一句:“这个方法未来会不会被改?”如果答案是“会”,那就说明设计可能有问题。尽量把可能变化的部分都抽象成接口,降低未来的破坏性变更概率。
4.5 快速识别清单
最后,我整理了一个快速识别清单,你可以直接拿来用。
- 编译期检查:跑一遍用户示例代码,看是否编译通过。
- 运行时检查:重点测边界值、异常分支、并发场景。
- 文档比对:新旧版本的API文档diff,关注废弃标记。
- 历史日志:如果SDK有埋点,看废弃API的调用量是否降为零。
嗯,今天就先聊到这儿。记住一句话:破坏性变更不可怕,可怕的是一声不吭地改。