模块接口设计:接口定义规范、接口版本管理、接口兼容性设计

接口设计这件事,说白了就是定规矩。你想想看,两个模块之间要通信,总得有个双方都认的协议吧?我做了十几年架构,见过太多因为接口没设计好导致的线上事故。今天咱们就聊聊接口设计的三个核心问题:怎么定义、怎么管版本、怎么保证兼容。

接口定义规范:别让接口变成"接囗"

接口定义规范,是模块化设计的基石。我个人习惯,每个接口必须包含三要素:语义、语法、协议

  • 语义:这个接口是干什么的?比如"获取用户信息"还是"更新用户密码"
  • 语法:数据怎么传?JSON还是Protobuf?字段名是什么?
  • 协议:用什么传输?HTTP、gRPC还是消息队列?

我在项目中遇到过最坑的事,就是两个团队各自理解接口语义。A团队认为"deleteUser"是软删除,B团队认为是硬删除。结果线上数据被物理删除了,恢复都恢复不了。从那以后,我要求所有接口定义必须附带行为描述文档,不能只看方法名。

接口定义规范清单

  • 命名规则:动词+名词,如 getUserById
  • 参数约束:必填/选填、类型、取值范围
  • 返回值结构:统一包裹层,如 { code, message, data }
  • 错误码定义:全局统一,不能各模块自己编
  • 幂等性说明:哪些接口可以重试,哪些不行

来看一个实际例子。我常用的RESTful接口定义规范是这样的:

// 好的接口定义
POST /api/v1/users
{
  "name": "张三",        // 必填,字符串,2-20字符
  "email": "zhang@example.com", // 必填,合法邮箱格式
  "age": 28              // 选填,整数,0-150
}
// 返回
{
  "code": 0,
  "message": "success",
  "data": {
    "userId": "u_12345",
    "createdAt": "2024-01-15T10:30:00Z"
  }
}

// 糟糕的接口定义
POST /api/addUser
{
  "name": "张三",
  "mail": "zhang@example.com"  // 字段名不规范,跟其他接口不一致
}
// 返回
{
  "id": 12345  // 没有状态码,没有错误信息
}

我的小技巧:接口定义完成后,让另一个团队的工程师读一遍。如果他能在5分钟内理解怎么调用,说明定义合格。否则,重写。

接口版本管理:别让升级变成灾难

接口版本管理,说白了就是给接口加个"版本号"。但怎么加、什么时候升版本、旧版本怎么处理,这里面门道不少。

我常用的版本管理策略有三种:

策略 实现方式 适用场景 我踩过的坑
URL路径版本 /api/v1/users, /api/v2/users 对外公开API 版本号膨胀,v1到v10都有
请求头版本 Accept: application/vnd.myapp.v1+json 内部服务间调用 调试时容易忘记加头
参数版本 /api/users?version=1 快速迭代阶段 参数容易被忽略

我个人习惯用URL路径版本。为什么?因为最直观。你想想看,运维看日志的时候,一眼就能看出请求的是哪个版本。我曾经用请求头版本,结果线上排查问题时,翻半天日志才发现版本号传错了。

警告:版本号一旦发布,就不要轻易删除。我曾经见过一个团队,v1接口用了半年就下线了,结果有个老客户还在用,直接导致业务中断。建议至少保留两个大版本,给调用方足够的迁移时间。

版本号的命名也有讲究。我推荐语义化版本

  • 主版本号:不兼容的API修改,比如删字段、改参数类型
  • 次版本号:向下兼容的功能新增,比如加个可选参数
  • 修订号:向下兼容的问题修正,比如修bug、优化性能

举个例子:从v1.2.3升到v2.0.0,说明有破坏性变更。调用方必须改代码。从v1.2.3升到v1.3.0,说明加了新功能,但老功能还能用。

接口兼容性设计:向前兼容是底线

接口兼容性设计,是模块化设计中最容易被忽视的环节。很多架构师只关注功能实现,不考虑接口变更对其他模块的影响。结果一升级,整个系统都崩了。

我总结了几条兼容性设计原则

  1. 只加不减:新版本只能加字段,不能删字段。老字段可以标记为"废弃",但不能直接删除。
  2. 宽松输入,严格输出:接收参数时尽量宽容,多传的参数忽略掉;返回数据时严格按规范,不多不少。
  3. 默认值策略:新增的字段必须提供默认值,保证老调用方不传也能正常工作。
  4. 枚举值只增不删:枚举类型只能加新值,不能改旧值含义,更不能删旧值。

避坑指南:我曾经在项目中把接口返回的"status"字段从字符串改成了数字。我觉得"0"和"success"差不多嘛。结果调用方用字符串比较,直接全挂了。从那以后,我改任何字段类型都会先发公告,给调用方至少两周的迁移时间。

来看一个兼容性设计的代码示例:

// v1版本接口
GET /api/v1/users/123
返回:
{
  "name": "张三",
  "email": "zhang@example.com"
}

// v2版本接口(兼容v1)
GET /api/v2/users/123
返回:
{
  "name": "张三",
  "email": "zhang@example.com",
  "phone": "13800138000",  // 新增字段,有默认值null
  "status": "active"       // 新增字段,默认值"active"
}

// 错误做法:v2直接删掉email字段
// 调用方会崩溃!

还有一个容易被忽略的点:接口的幂等性。说白了,就是同一个请求发多次,结果应该一样。我见过一个支付接口,因为没有做幂等处理,用户点了两次提交按钮,结果扣了两次钱。嗯,这个锅最后架构师背了。

我的经验:所有写操作接口都应该支持幂等。实现方式很简单:调用方传一个唯一请求ID,服务端用这个ID做去重。这样即使网络重试,也不会产生副作用。

最后说说接口废弃策略。接口不能永远不变,但也不能说废就废。我一般这样操作:

  • 在接口文档中标记"废弃",并注明替代方案
  • 保留废弃接口至少6个月
  • 返回响应时加个"Warning"头,提醒调用方该接口即将下线
  • 6个月后,如果调用量降到0,再考虑下线

你想想看,如果每个模块都遵守这些规范,整个系统的维护成本会降低多少?接口设计不是写代码,是定契约。契约写好了,大家按规矩办事,系统才能稳定运行。