SDK 架构设计原则:模块化、可扩展、易用性
说实话,刚开始设计 SDK 的时候,我觉得把功能都塞进一个类里,包装得漂漂亮亮就完事了。直到我维护的那个支付 SDK,因为耦合太紧,改一个渠道结果带崩三个,被用户追着骂了整整两周⋯⋯嗯,从那以后,我再也不敢小看架构设计了。这一章聊聊三个核心原则:模块化、可扩展、易用性。听起来都是老生常谈对吧?但你在项目里落地时,哪个坑都不会少。
原则不是教条,是用来解决真实问题的。在你动手写第一行接口之前,先想清楚这三点,后面改接口的次数会少很多。
模块化:清晰边界,各司其职
模块化说白了就是拆。但你拆的时候得问自己:每个模块有没有明确的职责?模块之间的依赖是不是单向的?我见过一个项目,把网络请求、数据解析、UI 绑定全塞在一个「工具类」里,4500 行代码。每次加一个字段都要全局搜一遍,生怕漏了哪里要改。这就是反面教材。
我个人习惯把 SDK 拆成三层:
- 核心层 — 提供基础能力,比如网络、日志、缓存。不依赖业务。
- 业务层 — 对 SDK 内部逻辑进行编排,比如支付流程、用户认证。
- 接口层 — 对外暴露的 API,薄薄的一层,只做参数校验和结果透传。
我在项目中遇到过一个问题:接口层直接调用了核心层的网络类。结果用户想自定义超时时长,我们改了核心层,结果接口层也跟着炸。后来我们用 Facade 模式 把接口层和核心层隔开,业务层只依赖接口抽象,不依赖具体实现。改起来就舒服多了。
// 不推荐:接口层直接依赖核心
public class PaymentSDK {
private NetworkClient client;
public void pay(int amount) {
client.post("payment", data); // 耦合太紧
}
}
// 推荐:通过抽象隔离
public interface PaymentGateway {
PaymentResult execute(PaymentRequest request);
}
public class PaymentSDK {
private PaymentGateway gateway;
public PaymentSDK(PaymentGateway gateway) {
this.gateway = gateway;
}
public void pay(int amount) {
gateway.execute(new PaymentRequest(amount));
}
}
模块化的核心信号:一个模块改了,最多只影响它的直接依赖者。如果你发现改了一个方法,要改五个调用方,说明边界没画对。
可扩展:预留变化,热插拔
SDK 发布出去之后,你不能要求用户每次都升级核心版本。你想想看,用户可能已经集成了你的 1.0 版本,现在想加一个自定义日志功能,你总不能说「请升级到 2.0」。可扩展性就是要让你在不破坏现有 API 的前提下,给用户「开个口子」。
我以前设计过一个图片加载 SDK,初期只支持 URL 加载。后来用户说「我想从本地文件加载」「我想从字节数组加载」。我每次都改核心代码,结果接口越变越臃肿。后来我换成了 插件式架构:基础功能内置,扩展功能通过接口注入。
public interface ImageLoader {
Image load(String source);
}
// 内置实现
public class UrlImageLoader implements ImageLoader { ... }
// 用户自定义实现
public class FileImageLoader implements ImageLoader {
@Override
public Image load(String source) {
return new FileImage(source);
}
}
// 使用
SDK.registerLoader("file", new FileImageLoader());
我曾经设计过过度抽象的扩展点。一个接口抽象了 6 个方法,用户实现起来比直接用核心 API 还复杂。可扩展不是让你把所有东西都做成插件,而是只在那些「确实会变化」的地方留口子。
记住一个原则:对扩展开放,对修改封闭。怎么判断哪些地方会变化?你可以看需求文档里那些「未来可能会⋯⋯」的描述,或者直接回顾历史版本中哪些接口改得最频繁。
易用性:用户不需要读文档
这点可能最容易被技术人忽略。你写了一个完美抽象的模块,扩展点也留好了,但用户打开 IDE 一看,三个类名类似的方法,参数类型还混着 int 和 String,第一个想法就是「这 SDK 很难用」。我有个朋友说他评估 SDK 的标准很简单:创建一个实例需要几步?超过三步的直接 pass。
我习惯用「五分钟上手」这个标准。如果一位新用户按着直觉敲代码,五分钟内能跑通一个最简单的用例,那接口设计就算及格了。这里面有几个关键点:
- 命名自解释:方法名应该直接告诉用户「我在做什么」。别用 execute、process、handle 这种万能动词。
- 参数少且合理:超过三个参数就要考虑用对象封装,而且参数顺序要符合直觉。
- 错误信息有温度:不要只返回一个错误码,要告诉用户「哪里错了」和「怎么改」。
| 易用性维度 | 糟糕设计 | 良好设计 |
|---|---|---|
| 创建实例 | 需要配置 config、、callback,再调用 init | new SDK(config) 就能用 |
| 调用方法 | process(String type, int flag, Object data) | uploadFile(String path) |
| 错误提示 | 返回 10086 | 返回 "超时: 网络连接超过 5 秒,请检查网络" |
| 异步处理 | 需要自己管理线程池 | 支持回调或返回 Future |
核心易用性检查清单:① 5 分钟内能否完成初始化?② 核心 API 方法名是否一看就懂?③ 常见错误是否能在两行内定位原因?
三者如何共存?
模块化、可扩展、易用性,单个拎出来都好理解,但放在一起就有取舍。模块化容易让接口变多,可扩展可能让初始化流程变复杂,易用性有时候会牺牲灵活性。我个人觉得 用户场景决定优先级:面向内部团队的 SDK,可扩展性可以放到第一位;面向普通开发者的公共 SDK,易用性一定是红线。
三种原则不缺不漏,全在你的设计里找平衡。下面这张图能帮你快速回顾三者之间的内核关系:
你可以拿这张图做你设计时的「原则墙」。每次开始写新接口前,先对着图问自己:这个接口拆得够细吗?扩展点该留还是不该留?用户第一次看到这个类名会不会懵?回答清楚了,再写代码。