3. SDK 接口设计原则:契约优先、松耦合、版本兼容

说实话,我见过太多迁移项目栽在“接口说改就改”上。有个项目,后端临时加了个字段,前端 SDK 直接崩溃。为什么?因为接口设计没守住底线。

这一章,我就聊聊自己这些年总结的三个核心原则。契约优先、松耦合、版本兼容。说白了,就是让你的 SDK 接口在复杂迁移环境中还能立得住。

3.1 契约优先:先谈好,再动手

什么叫契约优先?很简单。还没开始写代码,先定义好接口长什么样。我习惯用 OpenAPI 规范做接口描述文件。团队先评审这个文件,再各自开发。

好处很明显。

  • 前后端并行开发:前端可以 mock 数据,后端可以直接跑测试。
  • 减少扯皮:接口字段、类型、边界条件,白纸黑字写着。
  • 自动化测试:契约文件能直接生成客户端和服务端的测试桩。

我的习惯:每次接口改动,必须先改契约文件。代码可以不写,契约必须更新。否则,后面一定有人踩坑。

具体怎么做?

  1. 定义一个 YAML 或 JSON 格式的接口描述。
  2. 包含请求方法、路径、参数、请求体、响应体。
  3. 明确每个字段的含义、类型、是否必填、取值范围。
  4. 定义错误码和错误响应格式。
  5. 用工具(比如 swagger-codegen)生成客户端和服务端代码。
# example-api.yaml
openapi: "3.0.0"
info:
  title: 用户数据迁移接口
  version: "1.0.0"
paths:
  /users/{userId}:
    get:
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: 成功返回用户信息
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: 用户不存在
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
      required:
        - id
        - name

避坑指南:我曾经见过一个团队,契约文件里只写了接口路径和参数,没写响应体结构。结果前后端对字段名理解不同,联调时数据全乱了。契约必须细化到字段级别。

3.2 松耦合:别让 SDK 和业务绑死

松耦合这个原则,说起来容易做起来难。你想想看,SDK 如果直接依赖数据库实体类,但凡数据库表加个字段,SDK 就得重新发版。

我建议的做法:

  • 数据对象独立:SDK 内部维护自己的数据模型,不要引用外部系统的实体。哪怕底层数据库变了,SDK 的接口结构不变。
  • 适配器模式:数据进来,先经过适配器转换成 SDK 内部格式。这样外部系统的调整不会扩散到 SDK 核心逻辑。
  • 依赖注入:配置、日志、HTTP 客户端这些组件,通过接口注入,不要硬编码。
# 松耦合的客户端封裝
class MigrationClient:
    def __init__(self, http_client, config_provider):
        self._http = http_client
        self._config = config_provider

    def get_user(self, user_id: str) -> UserDTO:
        # 内部数据模型 UserDTO 独立于外部实体
        raw_user = self._http.get(f"/users/{user_id}")
        return self._adapt(raw_user)

    def _adapt(self, raw: dict) -> UserDTO:
        # 适配器转换
        return UserDTO(
            id=raw.get("user_id"),
            name=raw.get("full_name"),
            email=raw.get("email_address")
        )

说到底

核心要点:SDK 应该是独立的小王国。外面世界怎么变,内部逻辑不乱。你只需要改适配器那层代码。

有一次,我负责一个电商订单迁移项目。对方系统的订单表有 60 多个字段,我们 SDK 只关心 10 个。如果直接复用他们的实体,SDK 体积会膨胀,而且每次字段调整都得我们配合。后来我坚持用适配器模式,只映射需要的字段。项目上线后,对方改了三次表结构,我们一行代码没动。

3.3 版本兼容:向前走向后看

版本兼容,说白了就是旧版本 SDK 不能因为新接口上线就崩了。我踩过的最痛的坑是什么?当年我用过一个内部 SDK,每次版本升级,接口签名都改。结果我们团队要同时维护三套不同版本的适配代码。

后来我定了个规矩:SDK 必须遵循语义化版本。

版本号格式 兼容性 什么情况下升级 例子
MAJOR.MINOR.PATCH 大版本不兼容,小版本兼容 新增必填参数、删除字段、改变接口路径 1.0.0 → 2.0.0
MAJOR.MINOR.PATCH 小版本向后兼容 新增可选参数、新增响应字段 1.0.0 → 1.1.0
MAJOR.MINOR.PATCH 补丁版本完全兼容 修复 bug、性能优化 1.0.0 → 1.0.1

怎么做到向后兼容?

  • 参数只增不删:新参数必须设为可选,带默认值。
  • 响应体只增不减:新字段可以加,旧字段不能删。客户端如果只解析已知字段,就不会受影响。
  • 用枚举不要用 magic number:状态码、类型码,直接定义成枚举。新加枚举值不影响旧代码。
  • 保留旧接口:实在要改接口,保留旧版本 URL(比如 /v1/users 和 /v2/users),给客户端缓升时间。
# 版本兼容的接口设计
from enum import Enum

class UserStatus(Enum):
    ACTIVE = "ACTIVE"
    INACTIVE = "INACTIVE"
    # 新版本可以加,不影响旧代码
    SUSPENDED = "SUSPENDED"

@dataclass
class UserResponse:
    id: str
    name: str
    email: str
    status: UserStatus
    # 新增字段
    created_at: Optional[str] = None  # 可选,旧客户端忽略

避坑指南:我曾经见过一个项目,大版本升级时直接把老接口下掉了。结果有十几个客户端没来得及升级,直接挂了。记住,下掉旧接口前,必须确认所有客户端都已迁移。这个窗口期至少留一个完整迭代周期。

3.4 三个原则的关系

这三个原则不是孤立的。它们互相支撑,形成一个三角稳定结构。

SDK 接口设计三角原则 契约优先 松耦合 版本兼容 三者如何协作 契约优先 → 明确接口边界 松耦合 → 隔离变更影响 版本兼容 → 保障平滑升级

契约优先给了你一个明确的边界,松耦合帮你隔离了变更的冲击,版本兼容保障了过渡期的安全。三者缺一不可。

我个人的经验是:刚开始做 SDK 设计时,可以只关注契约。等接口稳定了,再逐步引入松耦合策略。版本兼容这个底线,从一开始就要守住。不然等项目跑起来再回头补,代价太大了。