4. SDK 架构设计:模块划分、接口设计、扩展性
聊到 SDK 架构,很多人第一反应是“把功能塞进几个文件里就行”。我早年也这么干过——结果项目还没上线,光接口变更就改了三轮。后来我学乖了:架构这事,得先想清楚模块怎么切、接口怎么给、未来怎么改。说白了,这是一劳永逸的投入。
4.1 模块划分:别让代码“揉成一团”
模块划分的核心原则是“高内聚、低耦合”。我习惯把 SDK 拆成四层:入口层、核心层、插件层、工具层。每层只管自己的事,不越界。
| 层次 | 职责 | 典型模块 |
|---|---|---|
| 入口层 | 对外暴露统一 API,处理参数校验、日志初始化 | AuditSDK, Builder |
| 核心层 | 负责密钥迁移、审计记录、数据校验 | Migrator, Auditor, Validator |
| 插件层 | 支持存储后端、加密算法、通知渠道的扩展 | StoragePlugin, CryptoPlugin, NotifyPlugin |
| 工具层 | 通用工具函数:时间戳、序列化、重试机制 | RetryUtil, JsonUtil, HashUtil |
你可能会问:为什么要把插件层单独拎出来?因为我踩过坑。有一次客户要求把审计日志写到 MongoDB,而原本只支持 MySQL。如果当时没有插件层,就得改核心代码,风险极大。有了插件层,加个实现类就行了,核心代码一行不动。
4.2 接口设计:少即是多,稳比快重要
接口是 SDK 的“脸面”。设计接口时,我通常会问自己三个问题:这个接口是不是必须的?调用方是不是容易误解?未来能不能兼容升级?
我个人习惯把接口分成两类:同步接口和异步接口。
- 同步接口:适用于小批量、快速返回的场景,比如单条密钥迁移。
- 异步接口:适用于大批量或耗时操作,比如全量审计。返回一个 Future 或回调。
下面是一个典型的接口示例(简化版):
public interface AuditSDK {
/** 同步迁移单条密钥 */
MigrateResult migrateKey(MigrateRequest request);
/** 异步批量迁移,返回任务ID */
String migrateKeysAsync(List<MigrateRequest> requests, Callback callback);
/** 查询迁移任务状态 */
TaskStatus queryTaskStatus(String taskId);
/** 注册自定义插件 */
void registerPlugin(Plugin plugin);
}
另外,返回值一定要设计成 Result 对象,包含状态码、消息、数据字段。别直接抛异常给调用方,让他自己 try-catch——那体验太差了。
4.3 扩展性:给未来留好“插槽”
扩展性靠什么?靠接口抽象和插件机制。说白了,就是让用户在不改你核心代码的前提下,替换任意组件。
我设计了一个 SPI(Service Provider Interface)机制。所有可扩展点都定义成接口,用户通过配置文件或代码注册自己的实现。比如存储插件接口:
public interface StoragePlugin {
/** 初始化存储连接 */
void init(StorageConfig config);
/** 写入审计记录 */
void write(AuditRecord record);
/** 批量读取 */
List<AuditRecord> read(QueryCondition condition);
}
用户只需实现这个接口,然后调用 sdk.registerPlugin(new MyMongoStorage()) 即可生效。
还有一个容易被忽略的点:事件总线。我在核心层加了事件发布机制,比如“迁移完成”、“审计失败”等事件。用户可以监听这些事件做额外处理(比如发送告警邮件)。这样既不入侵核心逻辑,又极大扩展了 SDK 的能力。
你想想看,如果没有插件层,每次新增存储类型都得改核心代码,还得重新编译发布。有了 SPI,用户自己写个 jar 包,配置一下就能用。这就是扩展性的价值。
至于代码层面的扩展性,我还有个习惯:所有关键逻辑都做成策略模式。比如迁移策略,默认用“先校验再迁移”,但你可以替换成“先备份再迁移”。接口定义好了,替换就是一行配置的事。
这篇内容来自我实际重构三次 SDK 的经验总结。记住:好的架构不是一次设计出来的,而是持续演进中“长”出来的。先把模块切清楚,接口给稳当,扩展槽留够,后面的事情会顺利很多。