4. SDK 架构设计:模块划分、接口设计、扩展性

聊到 SDK 架构,很多人第一反应是“把功能塞进几个文件里就行”。我早年也这么干过——结果项目还没上线,光接口变更就改了三轮。后来我学乖了:架构这事,得先想清楚模块怎么切、接口怎么给、未来怎么改。说白了,这是一劳永逸的投入。

4.1 模块划分:别让代码“揉成一团”

模块划分的核心原则是“高内聚、低耦合”。我习惯把 SDK 拆成四层:入口层、核心层、插件层、工具层。每层只管自己的事,不越界。

层次职责典型模块
入口层对外暴露统一 API,处理参数校验、日志初始化AuditSDK, Builder
核心层负责密钥迁移、审计记录、数据校验Migrator, Auditor, Validator
插件层支持存储后端、加密算法、通知渠道的扩展StoragePlugin, CryptoPlugin, NotifyPlugin
工具层通用工具函数:时间戳、序列化、重试机制RetryUtil, JsonUtil, HashUtil

你可能会问:为什么要把插件层单独拎出来?因为我踩过坑。有一次客户要求把审计日志写到 MongoDB,而原本只支持 MySQL。如果当时没有插件层,就得改核心代码,风险极大。有了插件层,加个实现类就行了,核心代码一行不动。

💡 我的建议:模块间通信只通过接口,不依赖具体实现。核心层不要直接 new 存储对象,而是通过工厂或依赖注入获取。

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);
}

👨‍🔧 经验之谈:参数对象用 Builder 模式,别用长长的方法参数列表。我见过一个方法有 8 个参数,调用方永远搞不清顺序。Builder 还能加默认值,兼容性更好。

另外,返回值一定要设计成 Result 对象,包含状态码、消息、数据字段。别直接抛异常给调用方,让他自己 try-catch——那体验太差了。

⚠️ 我曾经遇到的坑:接口里不要塞“临时开关”。比如加个 boolean enableDebug 参数,看起来灵活,但后期每个版本都在改默认值,调用方崩溃。这种配置应该放到 SDK 初始化时统一设置。

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 的能力。

SDK 架构模块关系图 入口层 (AuditSDK) 核心层 Migrator | Auditor | Validator | EventBus 插件层 (SPI) StoragePlugin | CryptoPlugin | NotifyPlugin 支持 MySQL / MongoDB / 本地文件 …… 工具层 RetryUtil | JsonUtil | HashUtil 每层只通过接口向下依赖

你想想看,如果没有插件层,每次新增存储类型都得改核心代码,还得重新编译发布。有了 SPI,用户自己写个 jar 包,配置一下就能用。这就是扩展性的价值。

至于代码层面的扩展性,我还有个习惯:所有关键逻辑都做成策略模式。比如迁移策略,默认用“先校验再迁移”,但你可以替换成“先备份再迁移”。接口定义好了,替换就是一行配置的事。

🔑 核心要点:模块划分决定代码怎么写,接口设计决定别人怎么用,扩展性决定项目能活多久。这三者环环相扣,缺一不可。

这篇内容来自我实际重构三次 SDK 的经验总结。记住:好的架构不是一次设计出来的,而是持续演进中“长”出来的。先把模块切清楚,接口给稳当,扩展槽留够,后面的事情会顺利很多。