2. SDK 架构设计原则:单一职责、开闭原则、依赖反转等
说实话,我刚入行那会儿,觉得设计原则就是些虚头巴脑的东西。代码能跑不就行了?后来被几个 SDK 项目狠狠教育过——维护成本飙升、接入方天天抱怨、改个功能得动半座山。
你想想看,SDK 是什么?是给别人用的。你写得爽没用,接入方用得爽才是王道。今天聊的三个原则,是我觉得最核心的。它们能帮你的 SDK 活得更久、改得更少、接得更顺。
2.1 单一职责原则:一个模块只干一件事
这个概念其实很简单:一个类、一个模块,只应该有一个让它改变的理由。
放到 SDK 里,就是每个功能模块的边界要清晰。登录模块只管登录,支付模块只管支付,别混在一起。
举个例子,我之前见过一个 SDK,它把一个「网络请求模块」写出了花——既管 HTTP 连接,又管数据缓存,还顺带做了日志上报。结果后来想升级网络库,发现日志上报也被牵连了,改了三天还没改完。
// 反例:一个模块干三件事
public class NetworkManager {
public void sendRequest(String url, Callback cb) { ... }
public void saveCache(String key, Object data) { ... }
public void uploadLog(String log) { ... }
}
// 正例:拆分职责
public class HttpClient {
public void request(String url, Callback cb) { ... }
}
public class CacheManager {
public void save(String key, Object data) { ... }
}
public class LogUploader {
public void upload(String log) { ... }
}
拆分之后的好处很明显:每个模块的改动范围可控,测试也容易写。你改网络库不会影响到缓存,改日志不会影响到请求。
2.2 开闭原则:对扩展开放,对修改关闭
这个原则听起来有点哲学。说白了就是:你想加新功能的时候,不要去改已经写好的老代码。
为什么会这样?因为老代码已经跑得很稳了,改它容易出 bug。而且 SDK 是给外部用的,你改了老代码,接入方就得跟着改——他们会骂人的。
// 不好的设计:每次加新支付类型都要改老代码
public class PaymentProcessor {
public void pay(String type, double amount) {
if (type.equals("alipay")) {
// 支付宝逻辑
} else if (type.equals("wechat")) {
// 微信逻辑
}
// 加一个银行卡支付?改这里!
}
}
// 好的设计:用抽象 + 注册机制
public interface PaymentMethod {
void pay(double amount);
}
public class AliPay implements PaymentMethod { ... }
public class WechatPay implements PaymentMethod { ... }
// 接入方自己注册
paymentEngine.register("alipay", new AliPay());
我在项目里遇到过一个典型场景:支付 SDK 一开始只支持支付宝和微信,后来要求加银行卡。用 if-else 的那个版本,改动风险很大。用接口抽象那个版本,只需要新增一个 BankCardPay 类就行了——一个字都不用改老代码。
2.3 依赖反转:面向接口编程,不要面向具体实现
这个原则的名字容易让人误解。它不是反转「依赖」本身,而是反转「依赖的方向」。
举个例子:你的 SDK 需要一个日志功能。你直接用了 Log4j 的 Logger 类,那么你的 SDK 就「依赖」了 Log4j。如果以后要换成别的日志库,你得改所有引用 Logger 的地方。
// 依赖具体实现 — 强耦合
public class LoginModule {
private Log4jLogger logger = new Log4jLogger();
public void login() {
logger.info("user login");
}
}
// 依赖接口 — 灵活解耦
public interface Logger {
void info(String msg);
}
public class LoginModule {
private Logger logger;
public LoginModule(Logger logger) {
this.logger = logger; // 依赖注入
}
public void login() {
logger.info("user login");
}
}
这个接口 Logger 是定义在 SDK 内部的。接入方只需要实现这个接口,就能用他们自己的日志库。SDK 对日志库没有直接依赖——依赖被「反转」了。
我曾经在做一个广告 SDK 时,网络库频繁换——从 OkHttp 换到 Retrofit,又换到 Volley。因为代码里到处都是 OkHttpClient,每次换网络库都得改几十个文件。后来我加了一个 HttpProvider 接口,接入方自己注入实现。从那以后,换网络库就像换衣服一样轻松。
2.4 接口隔离原则:小接口优于胖接口
这个原则很好理解:不要让你的接口包含太多方法。接入方只需要他们真正会用到的。
你看有些 SDK 的接口,动辄十几个方法,但大部分接入方可能只用两三个。剩下的都是无用的负担——文档要写、测试要跑、维护要管。
// 胖接口:接入方被迫实现很多方法
public interface UserService {
void login();
void logout();
void updateProfile();
void changePassword();
void bindMobile();
void uploadAvatar();
void getFriends();
}
// 隔离后的接口:按场景拆分
public interface LoginService {
void login();
void logout();
}
public interface ProfileService {
void updateProfile();
void uploadAvatar();
}
这样做的好处很直接:接口的契约小,改动的影响面就小。而且接入方可以根据需要选择实现哪个接口,不会被一堆不相关的方法绊住。
2.5 一张图读懂 SDK 架构原则
下面这张图,把上面讲的核心原则聚在一起。你看一眼就能记住它们的关系。
2.6 原则之间的平衡与取舍
这些原则不是孤立的。它们之间有时候会互相牵制。
比如,过分追求「单一职责」会导致模块数量爆炸;过度强调「开闭原则」可能让架构变得抽象难懂;「接口隔离」做过头,会让接入方集成一堆小接口。
| 原则 | 主要收益 | 常见陷阱 |
|---|---|---|
| 单一职责 | 模块稳定,改动范围小 | 拆得太细,管理成本高 |
| 开闭原则 | 新功能扩展快,老代码不改 | 过度抽象,引入不必要的复杂度 |
| 依赖反转 | 解耦,方便替换实现 | 接口数量过多,增加接入成本 |
| 接口隔离 | 接口瘦,接入方负担小 | 粒度太小,需要更多接口组合 |
我在项目里会有一个简单的判断标准:用户(接入方)的体验优先。如果某个原则让接入方用起来更痛苦,我会果断调整。原则是工具,不是枷锁。
1. 开闭原则(接入方最怕版本升级改代码)
2. 依赖反转(解耦是 SDK 的生命线)
3. 单一职责(稳定大于炫技)
4. 接口隔离(好用比完整重要)