2. SDK 架构设计原则与设计模式实战

说到 SDK 架构,我见过不少团队一上来就堆功能。结果呢?半年后需求一变,代码改不动,用户也抱怨接口难用。说白了,架构设计就是在「灵活」和「稳定」之间找平衡。下面这四个原则,是我这几年折腾 SDK 架构时踩坑踩出来的心得。

2.1 模块化:拆得开,合得拢

模块化不是什么新鲜概念,但很多 SDK 拆得并不干净。我见过一个协议迁移 SDK,把 HTTP 请求、数据解析、重试逻辑全塞在一个文件里,两千多行。后来要支持 WebSocket,改得那叫一个痛苦。

我个人习惯:按职责切分,每个模块只做一件事。比如我们的 SDK 至少分成这几块:

模块职责示例接口
传输层管理网络连接、协议转换send_request()
序列化层对象 ↔ 字节流serialize() / deserialize()
业务逻辑层重试、路由、回调handle_response()
公共工具层日志、配置、线程池Logger / Config

每个模块对外提供稳定接口,内部实现可以随便换。你想想看,如果传输层从 HTTP 改成 gRPC,只要接口不变,上层根本不用动。

2.2 可扩展性:别把路堵死

可扩展性,说白了就是「加功能不伤筋动骨」。我遇到过最糟糕的情况:SDK 的初始化函数只接受一个固定配置对象,后来要加自定义证书,只能改公共参数结构,影响了所有老用户。

我的做法是:面向接口编程,用插件思维设计扩展点。

关键扩展点举例:

  • 请求拦截器:允许用户在请求前后插入自定义处理(签名、日志、Mock)。
  • 传输层适配器:内置 HTTP + WebSocket,用户还可以自己实现 Transport 接口注入。
  • 序列化策略:默认 JSON,可替换为 Protobuf 或自定义格式。

代码上长这样:

# 传输层适配器接口
class Transport:
    def send(self, request: Request) -> Response:
        raise NotImplementedError

# 内置 HTTP 实现
class HttpTransport(Transport):
    def send(self, request: Request) -> Response:
        # ... 具体逻辑

# 用户自定义 gRPC 实现
class GrpcTransport(Transport):
    def send(self, request: Request) -> Response:
        # 用户自己实现

这样一来,SDK 的核心代码几乎不需要改,用户自己扩展就好。

2.3 易用性:让用户少读文档

业内有个观点:好的 SDK 应该是「直觉即正确」。我特别认同。以前我负责过一套支付 SDK,用户要到三个不同类里去拼参数。后来重构时我提了一个要求:80% 的调用场景,用户只需要对着一两个类设计。

容易踩的坑:

  • 接口返回 None 或裸异常,用户不知道哪里出错。
  • 配置项太多,没有合理默认值。
  • 异步和同步混在一起,命名不一致。

一个小技巧:提供一个「快速上手」的入口函数,把最常用的参数暴露出来,其他用默认值。用户有特殊需求再去翻高级文档。

举个例子,我们的迁移 SDK 有个 quick_start() 方法:

def quick_start(
    source_protocol: str = "http",
    target_protocol: str = "grpc",
    auto_retry: bool = True
) -> MigrationClient:
    # 内部自动配置传输层、序列化策略、重试策略
    return MigrationClientBuilder(...).build()

用户三行代码就能跑通。这才是易用性该有的样子。

2.4 向后兼容性:别让老用户炸掉

这个话题我最有的说。我曾经在一家金融科技公司,SDK 的某个方法签名从 do_something(a, b) 改成 do_something(a, b, c) 不加默认值。上线后,所有没用新参数的老用户全部编译失败——那天是周五,我加班到凌晨三点。

向后兼容的硬规则:

变更类型是否允许示例
新增参数(带默认值)✅ 允许func(a, b, c=None)
新增类、接口✅ 允许新加 NewFeatureClient
删除公开方法/类❌ 禁止标记 @deprecated,保留两个版本
修改方法返回值类型❌ 禁止用 Union 或子类扩展

另外,我会在文档和注解上明确标注 deprecated 的版本和推荐替代方案。例如:

@deprecated(since="2.0", removal="3.0", alternative="new_do_something")
def old_do_something():
    ...

用户看到警告就知道该升级了,不会突然某天被拦在门外。

2.5 设计模式在 SDK 中的应用

设计模式别硬套,但有些模式在 SDK 场景下确实好用。我挑三个最常用的说说。

2.5.1 工厂模式:屏蔽创建细节

用户不需要知道 HttpTransport 和 GrpcTransport 怎么初始化的,交给工厂:

class TransportFactory:
    @staticmethod
    def create(protocol: str, config: dict) -> Transport:
        if protocol == "http":
            return HttpTransport(config)
        elif protocol == "grpc":
            return GrpcTransport(config)
        else:
            raise ValueError(f"Unsupported protocol: {protocol}")

2.5.2 策略模式:运行时切换行为

比如重试策略。有的用户想要指数退避,有的直接不重试。我们可以定义策略接口:

class RetryStrategy:
    def should_retry(self, attempt: int, response: Response) -> bool:
        pass

class NoRetry(RetryStrategy):
    def should_retry(self, attempt, response) -> bool:
        return False

class ExponentialBackoff(RetryStrategy):
    def should_retry(self, attempt, response) -> bool:
        return attempt < 5

用户传入策略对象即可,SDK 本身不关心具体逻辑。

2.5.3 观察者模式:事件驱动解耦

SDK 内部有很多事件:连接断开、重试成功、迁移完成……把这些暴露给用户,他们可以做自己的监控或日志:

class MigrationEventBus:
    def register(self, event_type: str, callback: Callable):
        ...

    def emit(self, event_type: str, data: dict):
        ...

# 用户使用
bus = MigrationEventBus()
bus.register("connection_lost", lambda d: logging.warning(f"丢线: {d}"))

这就把 SDK 的核心行为和用户的自定义逻辑完全解耦了。

注意:设计模式别滥用。如果只用一个 if-else 能解决,就别硬套策略模式。我在代码评审里最常见的问题是「模式过度设计」——一个简单的配置判断非要搞出三四个接口。

好了,上面这四个原则和三种模式,基本覆盖了 SDK 架构的核心骨架。下面这张图可以帮你快速回顾它们之间的关系。

SDK 架构原则与设计模式关系图 模块化 高内聚·低耦合 可扩展性 插件·适配器 易用性 直觉即正确 向后兼容 不破旧用户 工厂模式 屏蔽对象创建细节 策略模式 运行时切换算法 观察者模式 事件解耦·松耦合 💡 实践建议 先定原则,再选模式。模块化是基础,可扩展性要靠适配器。 易用性和向后兼容是用户看不见但最能拉开差距的地方。

这张图把原则和模式放在一起看了。左侧是四大原则,右侧是对应的设计模式,注意每个原则不一定只对应一个模式,但工厂、策略、观察者是最常见的「三件套」。你在设计 SDK 时,不妨先确认自己要解决哪个原则的痛点,再去挑对应的模式。

我个人实践下来,只要把这几个方向梳理清楚,SDK 的代码质量至少上一个台阶。后面具体怎么搭传输层、序列化层、业务层,我们再一步步展开。