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 架构原则

下面这张图,把上面讲的核心原则聚在一起。你看一眼就能记住它们的关系。

SDK 核心设计原则关系图 SDK 架构 单一职责原则 每个模块只干一件事 开闭原则 对扩展开放,对修改关闭 依赖反转原则 面向接口,不面向实现 接口隔离原则 小接口优于胖接口 核心思想 变化不影响接入方,稳定胜过一切

2.6 原则之间的平衡与取舍

这些原则不是孤立的。它们之间有时候会互相牵制。

比如,过分追求「单一职责」会导致模块数量爆炸;过度强调「开闭原则」可能让架构变得抽象难懂;「接口隔离」做过头,会让接入方集成一堆小接口。

原则 主要收益 常见陷阱
单一职责 模块稳定,改动范围小 拆得太细,管理成本高
开闭原则 新功能扩展快,老代码不改 过度抽象,引入不必要的复杂度
依赖反转 解耦,方便替换实现 接口数量过多,增加接入成本
接口隔离 接口瘦,接入方负担小 粒度太小,需要更多接口组合

我在项目里会有一个简单的判断标准:用户(接入方)的体验优先。如果某个原则让接入方用起来更痛苦,我会果断调整。原则是工具,不是枷锁。

我的原则优先级:
1. 开闭原则(接入方最怕版本升级改代码)
2. 依赖反转(解耦是 SDK 的生命线)
3. 单一职责(稳定大于炫技)
4. 接口隔离(好用比完整重要)