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 的代码质量至少上一个台阶。后面具体怎么搭传输层、序列化层、业务层,我们再一步步展开。