第3章 SDK架构与组件介绍:模块划分、依赖关系、核心接口设计思路
说实话,拿到一个DSPM集成SDK,第一反应千万别急着看代码。我个人习惯先摸清它的骨架——模块怎么拆、谁依赖谁、接口是抽给谁用的。这套东西捋顺了,后面接入改造才不会踩坑。
几年前我第一次对接一个国产SDK,上来就改业务代码,结果编译通过了但运行就崩。后来发现核心模块和网络模块有循环依赖,编译期检测不出来,运行时才炸。从那以后,我再也不敢跳过架构分析这一步。
模块划分:拆得越清楚,改起来越舒服
大多数DSPM SDK都会遵循分层+领域驱动的方式拆模块。我见过比较合理的一种划分如下:
| 模块名 | 职责 | 典型组件 |
|---|---|---|
| Core(核心层) | 生命周期管理、上下文传递、调度引擎 | SDKContext、LifecycleManager、TaskScheduler |
| Network(网络层) | 通信协议封装、连接池、重试与超时 | HttpClient、WebSocketClient、RetryPolicy |
| Security(安全层) | 加密、签名、鉴权、证书管理 | CryptoProvider、Signer、AuthManager |
| Config(配置层) | 远程拉取配置、本地缓存、动态刷新 | ConfigFetcher、ConfigCache、ConfigListener |
| Monitor(监控层) | 性能采集、错误上报、日志聚合 | MetricsCollector、ErrorReporter、Logger |
| API(对外开放接口层) | 业务方法、数据模型、异常定义 | DataReportApi、ConnectionApi、SDKException |
依赖关系:别让模块“抱得太紧”
模块拆完了,下一步看依赖。这是最容易出鬼的地方。我记得有一次评审同事的PR,发现Monitor模块直接依赖了Network层的HttpClient——这是个典型的反向依赖。按规范,监控层应该只依赖Core和API,不能碰具体实现。
下面这张图是我自己整理的典型依赖关系,你们可以对照手头的SDK看看有没有跑偏。
你想想看,如果Monitor依赖Network,万一我们要切换网络库(比如从OkHttp换到Netty),监控代码也得跟着改,这不就耦合了么?正确的做法是:所有非API模块都只依赖Core,而Core不依赖任何具体实现,只定义抽象接口。Network、Security这些模块各自实现Core定义的接口,然后通过依赖注入组装。
核心接口设计思路:不过度设计,但要有弹性
接口设计这块,我吃过不少亏。早期我总喜欢“一次性把接口想全”,结果加了一堆没人用的参数。后来我学乖了——核心接口只解决三个问题:
- 初始化:把SDK启动起来,配置加载完毕,连接就绪。
- 数据上报:业务方只需传业务数据,剩下的压缩、加密、重试全由SDK内部搞定。
- 状态反馈:监听连接状态、错误信息、性能指标,方便业务做降级。
下面是我现在用的一份伪代码,实际项目也差不多这个味道:
public class DspmSdk {
// 初始化——参数链式构建,方便扩展
public static DspmSdk init(Config config) {
// 内部的Core会根据config自动组装各模块
return new DspmSdk(CoreFactory.create(config));
}
// 数据上报——异步非阻塞,调用方不用关心结果
public CompletableFuture<ReportResult> report(ReportData data) {
// 内部走拦截器链:压缩、加密、签名、发送、重试……
return core.getReportPipeline().execute(data);
}
// 状态监听——业务方可注册回调
public void setStateListener(StateListener listener) {
core.getStateManager().addListener(listener);
}
// 优雅关闭
public void shutdown() {
core.destroy();
}
}
避坑指南:接口粒度的取舍
举个例子。我曾经参与一个项目,设计师把数据上报接口拆成五个粒度:reportBinary、reportJson、reportBatch、reportStream、reportRaw。看起来很灵活对吧?现实是业务方根本搞不清用哪个,最后全用reportRaw传字符串,搞的SDK内部的压缩加密完全失效。
后来我们重构成了只留一个report(ReportData data),内部根据data的类型自动选择序列化和传输策略。接口变简单了,但内部可扩展性反而更强了。
模块间的双向依赖:能解则解,不惜代价
嗯,这里必须单独提一下双向依赖。我在做代码审计时,发现一些SDK为了“方便”,让Config模块直接回调Core模块的方法来刷新配置。这不就是双向依赖么?编译没问题,但你要做单元测试时就傻了——Mock来Mock去,测一个配置模块得拉起整个Core。
正确做法是引入事件总线或者观察者模式。Core只往外发事件,Config模块监听事件后自行更新,绝不反向调用Core的接口。我建议在架构层面就禁止任何双向依赖,哪怕增加一点代码复杂度也要拆掉。
小结一下
模块划分的核心是“高内聚、低耦合”,依赖关系要画一张清晰的单向图,接口设计要克制——少即是多。你想想看,如果能做到这三条,后续的接入改造至少能少踩70%的坑。我个人这几年接手的SDK,凡是架构混乱的,无一例外都是这三条没做好。
接下来,我们把目光放远一点。记住这张模块图,后面讲改造流程时,我会反复引用它。