SDK 架构设计原则
搞 SDK 设计,说白了就是在做一件事:给用户造一个好用的工具箱。工具箱里的每把扳手、每根螺丝刀,你都得想清楚怎么放、怎么拆、怎么加新工具进去。我个人习惯在动手写第一行代码前,先把几条核心原则在脑子里过一遍。它们不是什么高深理论,就是这么多年踩坑踩出来的经验。
今天重点聊聊三个原则:高内聚低耦合、可扩展性、可维护性。你想想看,如果 SDK 的模块像一坨浆糊,改一个地方崩十个接口,用户不骂你才怪。
高内聚 & 低耦合
先说高内聚。一个模块只做一件事,并且把这件事做到极致。我见过不少 SDK,一个类里塞了网络请求、数据解析、UI 渲染、文件缓存……美其名曰“全能类”,结果后期改一下网络协议,整个类都得翻一遍,测试用例加到吐。
举个例子,你写一个支付 SDK。支付流程本身、订单管理、回调处理、安全校验,这四个职责应该分到四个独立的模块里。每个模块内部只处理自己的逻辑,不关心别的模块怎么实现。
// ❌ 低内聚:一个类什么都干
class PaymentManager {
func pay(order: Order) { /* 发起支付 */ }
func validateSignature(data: Data) { /* 签名校验 */ }
func saveOrderToDB(order: Order) { /* 数据库操作 */ }
func showPaymentResultVC(result: PaymentResult) { /* UI 展示 */ }
}
// ✅ 高内聚:每个类只干一件事
class PaymentProcessor {
func execute(order: Order) { /* 发起支付 */ }
}
class SignatureValidator {
func validate(data: Data) -> Bool { /* 签名校验 */ }
}
class OrderRepository {
func save(order: Order) { /* 数据库操作 */ }
}
再来说耦合。模块之间依赖越少,后续改动的影响范围就越小。我在项目中遇到过一种典型场景:A 模块直接 new 了 B 模块的实例,还调了 B 的内部方法。后来 B 改了个构造函数签名,结果所有调用方都得跟着改。这就是高耦合带来的噩梦。
💡 我个人的判断标准:如果你改了一个模块的私有方法,需要同时修改超过 3 个其他模块的代码,那耦合度已经偏高了。赶紧抽接口、用依赖注入。
⚠️ 避坑指南:我曾经接手过一个 SDK,线程池直接写在网络模块里,结果 UI 模块为了在子线程回调刷新界面,硬生生去 import 了那个线程池对象……这不是耦合,是拧在一起。正确的做法是定义回调接口,让使用者自己决定线程策略。
可扩展性
SDK 和业务代码不一样。用户集成你的 SDK 后,不会隔三差五让你改版本。所以你的接口设计必须能容纳未来的变化。说白了,你要预判用户会怎么用,但不要过度设计。
我常用的手法是策略模式 + 配置化。比如日志组件,一开始只输出到控制台。但你知道用户肯定有需要写文件、传远程、或者加密的需求。那就在设计时预留一个 LoggerProvider 接口,让使用者可以注入自己的日志实现。
protocol LoggerProvider {
func log(level: LogLevel, message: String)
}
class SDKLogger {
private var provider: LoggerProvider
init(provider: LoggerProvider) {
self.provider = provider // 用户自定义
}
func debug(_ msg: String) { provider.log(level: .debug, message: msg) }
}
你看,这个设计让用户不需要改 SDK 代码,就能扩展日志行为。可扩展性不是让你把所有可能的功能都做出来,而是留一个干净的扩展点。
还有个容易被忽略的点:版本兼容。很多 SDK 升级时,直接把旧接口删了。用户升级后编译报错,心里一万句脏话。我建议至少保留一个 deprecate 周期。可以在旧接口上加 @available 标记,内部转发到新实现。
| 扩展方式 | 说明 | 适用场景 |
|---|---|---|
| 接口继承/协议 | 定义抽象基类或协议,用户实现具体逻辑 | 行为可被替换(日志、存储、加密) |
| 插件/SPI 机制 | 提供注册中心,运行时加载插件 | 大型 SDK 功能模块化(如网络层拦截器) |
| 配置化 | 通过属性或字典控制内部行为 | 开关、超时、重试次数等固定参数 |
| 回调/委托 | 用户设置代理,SDK 在关键节点通知 | 生命周期事件、状态变化 |
可维护性
SDK 不是写完就完了,后期会有 bug 修复、性能优化、新平台适配。代码可读性差,维护成本比开发成本还高。我记得有一次改一个老 SDK,一个函数 800 行,注释全是一年前写的“// 这里暂时这样”,我差点把电脑砸了。
可维护性体现在几个方面:
- 命名规范:类名、方法名要让人一看就知道是干什么的。用 `fetchUserProfile` 比 `getData` 好一百倍。
- 单一职责:一个方法别超过 30 行。如果你写了一个长方法,试着拆成几个小步骤,每个步骤用清晰的函数名描述。
- 注释时机:只注释“为什么这么做”,不注释“做了什么”。代码本身应该能自解释做了什么。
- 测试覆盖率:每条接口至少要有单元测试。我曾经在一个线上事故里发现,某个核心函数在重构时少了一行边界检查,结果导致部分设备崩溃。如果当时有测试,就不会漏掉。
🔑 核心观点:可维护的本质是你休假回来,甚至一年后再看自己写的代码,能在一小时内理解整个模块,而不是花两天重新考古。
你可能会问:这三条原则互相冲突怎么办?比如为了可扩展加了太多抽象,反而降低了可维护性。嗯,这里要平衡。小 SDK 优先可维护,大 SDK 优先可扩展。我个人习惯用“三次重构法则”:第一次实现核心功能,先保证可维护;第二次遇到类似需求时再提取抽象;第三次才考虑插件化。不要一开始就想着炫技。
⚠️ 避坑指南:我曾经在 SDK 早期就引入了依赖注入容器 + 动态代理 + 注解处理器,结果框架本身占了 SDK 一半的体积,用户集成后包体积暴涨。后来全砍了,换成简单的工厂方法。可扩展性够用就行,过度设计是灾难。
好了,总结一下我今天讲的精华:高内聚低耦合让模块边界清晰,可扩展性让 SDK 能适应变化,可维护性让你自己和团队不掉头发。这三条原则不是教条,而是在每个设计决策时拿来问自己的尺子。下次你再写一个类,心里多问一句:这个类是不是只干一件事?这个接口是不是最简?这段代码三个月后我还看得懂吗?
记住,好的 SDK 设计,不是功能最多,而是让集成者觉得“就该这么简单”。