SDK 架构设计原则:模块化、可扩展、易用性

说实话,刚开始设计 SDK 的时候,我觉得把功能都塞进一个类里,包装得漂漂亮亮就完事了。直到我维护的那个支付 SDK,因为耦合太紧,改一个渠道结果带崩三个,被用户追着骂了整整两周⋯⋯嗯,从那以后,我再也不敢小看架构设计了。这一章聊聊三个核心原则:模块化、可扩展、易用性。听起来都是老生常谈对吧?但你在项目里落地时,哪个坑都不会少。

原则不是教条,是用来解决真实问题的。在你动手写第一行接口之前,先想清楚这三点,后面改接口的次数会少很多。

模块化:清晰边界,各司其职

模块化说白了就是拆。但你拆的时候得问自己:每个模块有没有明确的职责?模块之间的依赖是不是单向的?我见过一个项目,把网络请求、数据解析、UI 绑定全塞在一个「工具类」里,4500 行代码。每次加一个字段都要全局搜一遍,生怕漏了哪里要改。这就是反面教材。

我个人习惯把 SDK 拆成三层:

  • 核心层 — 提供基础能力,比如网络、日志、缓存。不依赖业务。
  • 业务层 — 对 SDK 内部逻辑进行编排,比如支付流程、用户认证。
  • 接口层 — 对外暴露的 API,薄薄的一层,只做参数校验和结果透传。

我在项目中遇到过一个问题:接口层直接调用了核心层的网络类。结果用户想自定义超时时长,我们改了核心层,结果接口层也跟着炸。后来我们用 Facade 模式 把接口层和核心层隔开,业务层只依赖接口抽象,不依赖具体实现。改起来就舒服多了。

// 不推荐:接口层直接依赖核心
public class PaymentSDK {
    private NetworkClient client;
    public void pay(int amount) {
        client.post("payment", data); // 耦合太紧
    }
}

// 推荐:通过抽象隔离
public interface PaymentGateway {
    PaymentResult execute(PaymentRequest request);
}

public class PaymentSDK {
    private PaymentGateway gateway;
    public PaymentSDK(PaymentGateway gateway) {
        this.gateway = gateway;
    }
    public void pay(int amount) {
        gateway.execute(new PaymentRequest(amount));
    }
}

模块化的核心信号:一个模块改了,最多只影响它的直接依赖者。如果你发现改了一个方法,要改五个调用方,说明边界没画对。

可扩展:预留变化,热插拔

SDK 发布出去之后,你不能要求用户每次都升级核心版本。你想想看,用户可能已经集成了你的 1.0 版本,现在想加一个自定义日志功能,你总不能说「请升级到 2.0」。可扩展性就是要让你在不破坏现有 API 的前提下,给用户「开个口子」。

我以前设计过一个图片加载 SDK,初期只支持 URL 加载。后来用户说「我想从本地文件加载」「我想从字节数组加载」。我每次都改核心代码,结果接口越变越臃肿。后来我换成了 插件式架构:基础功能内置,扩展功能通过接口注入。

public interface ImageLoader {
    Image load(String source);
}

// 内置实现
public class UrlImageLoader implements ImageLoader { ... }

// 用户自定义实现
public class FileImageLoader implements ImageLoader {
    @Override
    public Image load(String source) {
        return new FileImage(source);
    }
}

// 使用
SDK.registerLoader("file", new FileImageLoader());

我曾经设计过过度抽象的扩展点。一个接口抽象了 6 个方法,用户实现起来比直接用核心 API 还复杂。可扩展不是让你把所有东西都做成插件,而是只在那些「确实会变化」的地方留口子。

记住一个原则:对扩展开放,对修改封闭。怎么判断哪些地方会变化?你可以看需求文档里那些「未来可能会⋯⋯」的描述,或者直接回顾历史版本中哪些接口改得最频繁。

易用性:用户不需要读文档

这点可能最容易被技术人忽略。你写了一个完美抽象的模块,扩展点也留好了,但用户打开 IDE 一看,三个类名类似的方法,参数类型还混着 int 和 String,第一个想法就是「这 SDK 很难用」。我有个朋友说他评估 SDK 的标准很简单:创建一个实例需要几步?超过三步的直接 pass。

我习惯用「五分钟上手」这个标准。如果一位新用户按着直觉敲代码,五分钟内能跑通一个最简单的用例,那接口设计就算及格了。这里面有几个关键点:

  • 命名自解释:方法名应该直接告诉用户「我在做什么」。别用 execute、process、handle 这种万能动词。
  • 参数少且合理:超过三个参数就要考虑用对象封装,而且参数顺序要符合直觉。
  • 错误信息有温度:不要只返回一个错误码,要告诉用户「哪里错了」和「怎么改」。
易用性维度糟糕设计良好设计
创建实例需要配置 config、、callback,再调用 initnew SDK(config) 就能用
调用方法process(String type, int flag, Object data)uploadFile(String path)
错误提示返回 10086返回 "超时: 网络连接超过 5 秒,请检查网络"
异步处理需要自己管理线程池支持回调或返回 Future

核心易用性检查清单:① 5 分钟内能否完成初始化?② 核心 API 方法名是否一看就懂?③ 常见错误是否能在两行内定位原因?

三者如何共存?

模块化、可扩展、易用性,单个拎出来都好理解,但放在一起就有取舍。模块化容易让接口变多,可扩展可能让初始化流程变复杂,易用性有时候会牺牲灵活性。我个人觉得 用户场景决定优先级:面向内部团队的 SDK,可扩展性可以放到第一位;面向普通开发者的公共 SDK,易用性一定是红线。

三种原则不缺不漏,全在你的设计里找平衡。下面这张图能帮你快速回顾三者之间的内核关系:

SDK 三大架构原则关系图 SDK 三大架构原则关系图 SDK 接口 模块化 独立测试 · 单一职责 接口契约 · 实现隔离 可扩展 插件架构 · 扩展点 配置注入 · 热插拔 易用性 自解释 API · 少参数 错误友好 · 快速上手 三个原则相互支撑:模块化让可扩展更容易,易用性反过来验证模块边界是否合理

你可以拿这张图做你设计时的「原则墙」。每次开始写新接口前,先对着图问自己:这个接口拆得够细吗?扩展点该留还是不该留?用户第一次看到这个类名会不会懵?回答清楚了,再写代码。