SDK 整体架构设计原则
做 SDK 的第一件事,不是写代码,而是想清楚怎么“切”。
说白了,就是把一个庞然大物,切成小模块,再让它们互相配合。
我做了快十年 SDK,踩过的坑一半都跟架构有关。
今天聊聊三个最核心的原则:模块分层、依赖倒置、接口隔离。
模块分层
分层是我见过最直观的武器。
把 SDK 分成几层,上层只管调用,下层只管干活。
比如网络层、业务层、UI 层,各管各的。
为什么要分层?
因为不分层,代码就会变成一团乱麻。
改动一个地方,牵扯三个模块,你怕不怕?
我在项目中遇到过一个真实场景:
有个支付 SDK 没有分层,业务逻辑和网络请求混在一起。
后来换了一家三方支付,结果改了半个月,还修了一堆 bug。
如果当时分了层,换底层实现根本不用碰业务代码。
来看一个典型的分层示例:
| 层次 | 职责 | 典型类/接口 |
|---|---|---|
| 表现层 | 对外暴露 API,处理入参出参 | PublicApi |
| 业务层 | 核心逻辑,状态机,流程编排 | PaymentService |
| 数据层 | 网络请求、本地缓存、文件读写 | HttpClient, CacheManager |
| 基础设施层 | 日志、线程池、加密 | Logger, ThreadPool |
依赖倒置
依赖倒置原则说:要依赖抽象,不要依赖具体实现。
你想想看,底层实现换了,上层代码要不要跟着改?
不该。
怎么做到?
上层定义接口,下层实现接口。
上层只管说“我要一个存储服务”,下层把文件存到本地还是云端,上层不管。
代码更容易看懂:
// 错误 demo:上层依赖具体实现
class UserService {
private FileStorage storage = new FileStorage(); // 耦合死了!
}
// 正确 demo:依赖接口
interface IStorage {
void save(String key, byte[] data);
byte[] load(String key);
}
class UserService {
private IStorage storage; // 通过构造器注入
public UserService(IStorage storage) {
this.storage = storage;
}
}
接口隔离
接口隔离说白了就是:别让调用方看见它不需要的东西。
一个臃肿的接口,调用方用不上,却还得依赖它,改接口时所有实现类都爆炸。
举个例子:
一个认证模块,只提供 login()、logout() 就够了。
如果你把 sendSms()、verifyCaptcha() 也塞进去,那所有依赖认证模块的调用方,都被迫跟着变。
正确的做法:拆成细粒度小接口。
每个调用方只依赖自己需要的那一块。
| 反例(胖接口) | 正例(隔离接口) |
|---|---|
AuthService { login, logout, sendSms, verifyCaptcha, resetPassword } |
ILogin { login }ILogout { logout }ISms { sendSms } |
| 所有调用方全依赖同一个大接口 | 每个调用方只依赖自己需要的接口 |
| 修改接口影响范围大 | 修改某个接口不影响其他调用方 |
三个原则的关系
模块分层是骨架,依赖倒置是肌肉,接口隔离是表皮。
骨、肉、皮缺一不可。
下面这张图展示了它们如何相互配合:
这张图里:
左侧是分层结构,从上到下调用;
中间是依赖倒置,两层之间的调用通过接口实现,而非直接依赖具体类;
右侧是接口隔离,每个接口只暴露最少的方法。
其实这三个原则并不是孤立存在的。
模块分层提供了物理边界;依赖倒置保证了逻辑方向;接口隔离保证了接口粒度。
我在很多项目里发现,只要把这三条落实到位,后期维护成本至少降一半。
嗯,今天的内容就到这。
下次写代码前,先画张图,问自己:
分层了吗?依赖抽象了吗?接口够小了吗?
然后你会发现,bug 少了一半。