第三章:SDK 设计原则——把地基打牢
聊完 SDK 的边界与模块划分,咱们得认真谈谈那些绕不开的设计原则。说实话,我刚入行那几年,总觉得原则是书本上教条的东西,项目一忙就忘了。直到有一次我负责的支付 SDK 被接入方投诉说“你们这个接口怎么又返回两种格式的错误码啊?”,我才意识到——原则不是用来背的,是用来救命的。
这一章我们就抓三个最核心的:单一职责、开闭原则、接口隔离。你想想看,这三条能搞定 SDK 里 80% 的维护噩梦。
核心观点:SDK 设计原则不是装饰,是长期维护成本的分水岭。违背原则一时爽,重构火葬场。
3.1 单一职责原则(Single Responsibility Principle)
说白了就是一个类、一个模块、一个接口,只干一件事。我给很多团队做过代码审查,最常见的毛病就是一个“Util”类里塞了几千行,从加密到日志到网络,全搅在一起。你改加密,不敢动日志,因为你压根不知道有没有隐藏依赖。
我个人习惯给每个职责画一个矩形框,框内只放同类行为。举个例子,一个日志上传 SDK,我会拆成三块:
- 日志收集器——只负责从应用层拉日志,不关心怎么传。
- 日志格式化器——只负责把原始数据转成 JSON 或 Protobuf。
- 日志上传器——只负责网络传输,重试、超时、断网续传都在这里。
每个模块可以独立测试、独立替换。我曾在项目中遇到一个坑:一个“全功能”类里同时做了数据缓存和网络请求,结果缓存层想换成 Redis 时,网络请求代码也得跟着改,因为共享了一些内部状态。嗯,从那以后我勒令团队:一个类只允许一个变化的理由。
实战经验:如果你发现自己在一个 .h 或 .java 文件里写了一大段注释来解释“这个类既做 A 又做 B”,那么十有八九已经在违背单一职责了。拆!
3.2 开闭原则(Open-Closed Principle)
对扩展开放,对修改关闭。这句话你可能听过无数遍,但落地时经常翻车。我曾经负责的推送 SDK,初期只支持 HTTP 长轮询,后来要加 WebSocket 支持。如果所有连接逻辑都写在一个类里,就得反复改那个类,改一次测一次,生怕打乱原有功能。
正确的做法是定义一个抽象接口 ConnectionProvider,然后分别实现 HttpPollingProvider 和 WebSocketProvider。新增协议时,写一个新实现类就行,不用动任何现有代码。这就是“关闭修改、开放扩展”。
// 抽象接口 —— 不允许被修改
public interface ConnectionProvider {
void connect();
void disconnect();
void send(byte[] data);
}
// 已有实现 —— 稳定
public class HttpPollingProvider implements ConnectionProvider {
// 具体实现
}
// 新扩展 —— 新增,不修改旧代码
public class WebSocketProvider implements ConnectionProvider {
// 具体实现
}
你想想看,这样设计以后,测试也简单:只测新加的 Provider,原有的单元测试不用动。我建议在 SDK 的核心模块里,所有可能变化的地方都用接口或抽象类做一层屏障。
注意陷阱:不要为了追求“未来扩展”而过度抽象。我见过有人给一个只有两个参数的 Init 方法也搞了个接口,结果后来三年都没第二个实现,反而让调用方多了一层间接理解成本。开闭原则要用在真正会变化的地方。
3.3 接口隔离原则(Interface Segregation Principle)
这个原则说的是:客户端不应该被强迫依赖它不使用的方法。举个例子,你做一个支付 SDK,可能同时需要第三方登录、余额查询、退款等功能。如果把所有方法塞进一个 PaymentSDK 接口,那么只做支付的接入方也要实现 balanceQuery() 和 refund(),不实现也得留下空方法或者抛出异常,体验极差。
我以前接手过一个广告 SDK,它的初始化接口 parameters 竟然包含十几个字段,有些字段只对特定广告平台有效。结果接入方文档都看不懂,每次上线前都得我们远程协助填参数。后来我把它拆成了最小接口集合:
| 大接口(勿用) | 小接口(推荐) |
|---|---|
AdSDK 含 12 个方法 |
BannerAdProvider 3 个方法 |
PaymentSDK 含 8 个方法 |
PaymentProvider 4 个方法 + RefundProvider 2 个方法 |
LogSDK 含 6 个方法混了上传和配置 |
LogPublisher 2 个方法 + LogUploader 2 个方法 |
接口隔离的本质是“按需提供”。每个接入方只看到和它相关的那几个方法,文档变薄,学习成本降低。嗯,这里要注意:不要隔离得太碎,否则一个业务需要实现好几个接口,反而增加适配复杂度。我建议以调用者的角色视角来划分——比如买家角色只需要 Payment 和 Order 接口,卖家角色才需要 Refund 和 Settlement。
// 错误示范:胖接口
public interface PaymentSDK {
void pay();
void refund();
void queryBalance();
void login();
void logout();
}
// 正确示范:按角色隔离
public interface PaymentProvider {
void pay();
}
public interface RefundProvider {
void refund();
}
public interface UserAuthProvider {
void login();
void logout();
}
技巧:如果你在编写接口文档时,发现某个方法前面要写“如果使用了 A 功能,则忽略此参数;如果使用了 B 功能,则必须传…”,这就是接口隔离的信号。拆,别犹豫。
下面这张图展示了三个原则如何共同作用,构成一个稳定的 SDK 架构基座:
3.4 三个原则的综合运用技巧
在实际项目中,这三个原则并不是孤立使用的。我通常按这个顺序来思考:
- 先找职责边界(SRP):把 SDK 的功能切成几个互不重叠的责任区。
- 再定义抽象(OCP):对每个责任区抽象出接口,确保未来的变化通过新增实现完成。
- 最后裁剪接口大小(ISP):把接口按调用者角色拆成多个小接口,避免胖接口污染。
举个例子,你要设计一个存储 SDK:
- SRP:数据读写一个类、缓存策略一个类、文件管理一个类。
- OCP:抽象出
StorageProvider、CacheStrategy接口。 - ISP:只给只读场景暴露
ReadOnlyStorage接口,而不是整个读写接口。
避坑指南:我曾经在一个项目里同时应用所有原则,结果接口层级深达 7 层,新人看代码直接崩溃。记住:原则是指导,不是束缚。小 SDK 或内聚度很高的模块,适度简化,不要为了原则而原则。