第三章: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 架构基座:

SDK 设计原则知识结构图 SDK 设计原则 单一职责 (SRP) 开闭原则 (OCP) 接口隔离 (ISP) 长期可维护 · 低耦合 · 高内聚 · 易扩展 三个原则互相支撑:SRP 让模块职责清晰 → OCP 保证扩展不改旧代码 → ISP 确保接口按需提供

3.4 三个原则的综合运用技巧

在实际项目中,这三个原则并不是孤立使用的。我通常按这个顺序来思考:

  1. 先找职责边界(SRP):把 SDK 的功能切成几个互不重叠的责任区。
  2. 再定义抽象(OCP):对每个责任区抽象出接口,确保未来的变化通过新增实现完成。
  3. 最后裁剪接口大小(ISP):把接口按调用者角色拆成多个小接口,避免胖接口污染。

举个例子,你要设计一个存储 SDK:

  • SRP:数据读写一个类、缓存策略一个类、文件管理一个类。
  • OCP:抽象出 StorageProvider、CacheStrategy 接口。
  • ISP:只给只读场景暴露 ReadOnlyStorage 接口,而不是整个读写接口。

避坑指南:我曾经在一个项目里同时应用所有原则,结果接口层级深达 7 层,新人看代码直接崩溃。记住:原则是指导,不是束缚。小 SDK 或内聚度很高的模块,适度简化,不要为了原则而原则。