接口设计规范:命名、参数与返回值
讲接口设计,绕不开三个基础问题:叫什么、传什么、返回什么。我见过不少团队,功能做得很牛,接口却写得让人想骂娘。命名随心所欲、参数堆成山、返回值藏着各种“惊喜”。
这一章咱们就来聊聊,怎么把接口设计得像一本说明书,而不是猜谜游戏。我会结合自己踩过的坑,给你一套可落地的规范。
1. 命名规范:让人一看就懂
接口命名是给开发者看的,不是给机器看的。我见过一个SDK,里面有个方法叫 doSomething,你猜它是做什么的?反正我猜不出。后来翻了十几页文档才明白,原来是初始化某个模块。
命名第一条:清晰胜过一切。
我个人习惯用一组固定的动词前缀。这样调用者看到开头就知道意图:
// 推荐:动词 + 名词结构
int getUserProfile(int userId);
void saveUserSettings(const UserSettings& settings);
bool deleteAccount(const std::string& accountId);
// 不推荐:让人猜
int getSomething(int id);
void handleData(const char* data);
bool cleanUp();
为什么一定要这样?你想想看,当你在代码提示里看到 get、create、delete 开头的方法,是不是一秒就知道怎么用?这就是规范的价值。
我常用的动词表:
get— 获取单个资源list— 获取资源列表create— 创建资源update— 更新资源(全量)patch— 部分更新资源delete— 删除资源
不要自己发明动词,比如 fetch、retrieve、obtain,统一用 get 就够了。
命名的第二点是风格一致性。 驼峰、下划线、匈牙利命名法,选一个并坚持到底。我建议SDK用小驼峰(方法名首字母小写),因为大多数语言的SDK都这么干。你看AWS、Google的SDK,清一色小驼峰。
第三,不要缩写得太狠。 像 usrInf 这种,省了三个字母,多了十分钟的猜测时间,不值。代码是写给人读的,编译器才不关心名字长短。
我曾经踩过的坑:
有一次我改了一个线上SDK,发现一个方法叫 getUid()。我以为是返回用户ID,结果它返回的是会话ID。为什么?因为当初写代码的人觉得 sessionId 太长了。后来这个接口导致线上故障,被用户追着骂了两周。
从此以后,我定了一条死规矩:命名必须是全称,不允许自行缩写。
2. 参数设计:少即是多
参数设计这块,我见过最离谱的一个接口,签名长这样:
int doBusiness(int a, char b[], int c, float d, bool e,
char f[], int g, double h, int i, bool j);
这是个真实案例。调用这个接口时,你得数着哪个参数是干什么的,稍微少传一个变量,编译都过不去。这哪是接口设计,这就是在折磨你的用户。
参数设计的黄金法则:不超过3个参数。 超过3个,就用结构体或者对象包装一下。
// 不好的设计:参数太多
bool sendMessage(const char* to, const char* subject,
const char* body, int priority, bool requireAck,
int retryCount, int timeout);
// 好的设计:用对象封装
struct MessageRequest {
std::string to;
std::string subject;
std::string body;
int priority = 1;
bool requireAck = false;
int retryCount = 3;
int timeout = 5000;
};
bool sendMessage(const MessageRequest& request);
为什么要这么改?参数顺序错了怎么办?少传了一个怎么办?对象封装后,调用者可以用 Builder 模式 只传必填项,其他用默认值,代码可读性直接起飞。
我建议的参数设计检查清单:
- 必填参数放前面,可选参数放后面(或用默认值)
- 布尔参数单独拎出来,不要埋在数字参数中间
- 如果参数之间存在逻辑关联,用结构体捆绑
- 不要用
void*或Object传任意类型——这是给自己挖坑
还有一个容易被忽略的点:参数类型要选择合理范围。 比如表示月份,用 int 的话,调用者可能传个13进去。你应该用枚举:
enum class Month : uint8_t {
Jan = 1, Feb, Mar, Apr, May, Jun,
Jul, Aug, Sep, Oct, Nov, Dec
};
void setMonth(Month m); // 永远不会有非法值
3. 返回值设计:明确承诺,处理异常
这一块是很多SDK翻车的地方。返回值设计不好,调试能搞到你想摔键盘。我见过一个接口,成功时返回0,失败时返回-1,异常时抛异常。调用者要写三个不同的处理逻辑,结果还是经常漏掉异常分支。
返回值设计的核心:类型明确,错误可预期。
我个人倾向使用统一的返回结构。不管是成功还是失败,都走同一个路径,调用者不需要判断 if (ret == -1) 还是 catch。
// 统一返回结构
struct Result<T> {
bool success;
T data; // 成功时填充
ErrorCode error; // 失败时填充
std::string message; // 可读的错误描述
};
// 调用方式
auto result = getUserProfile(userId);
if (result.success) {
// 处理 result.data
} else {
// 处理 result.error 和 result.message
}
为什么我觉得这种方式更好? 它把分支逻辑变成了线性的:要么处理数据,要么处理错误。不会出现“成功返回0,失败返回-1,资源不存在返回404”这种需要记忆的规则。
| 设计方式 | 优点 | 缺点 |
|---|---|---|
| 纯错误码 | 性能高,轻量 | 调用者容易忽略返回值,错误信息不直观 |
| 异常抛出 | 调用者可不处理(不推荐) | 性能开销大,调用链复杂,容易忘记捕获 |
| 统一 Result 结构 | 强制处理错误,信息丰富,可读性好 | 需要复制错误码,有点啰嗦 |
一个重要的原则:返回类型不要用布尔值表达“成功/失败”。 布尔值只告诉你“成功”和“不成功”,但“不成功”的原因是什么?是参数错了?资源不存在?还是服务器挂了?布尔值根本说不清楚。
我曾经踩过的坑(二):
有一次写一个文件上传SDK,返回类型是 int。文档上写:0表示成功,1表示失败,2表示文件太大,3表示网络错误。结果一个后端开发偷偷加了返回值4表示“校验失败”,但是没有更新文档。用户看到一个4,翻了半天文档找不到解释,气得直接弃用了整个SDK。
我后来总结:错误的类型和描述,必须跟着返回值一起走。用户拿到错误,应该能立刻知道发生了什么,以及可以怎么解决。
还有一点,不要返回裸指针或引用。 让我用户自己去管理生命周期,那是不负责任的。能用 shared_ptr 就用,不能的话返回值拷贝。谁都不想因为忘记 delete 导致内存泄漏。
// 不要这样
User* getUserById(int id);
// 要这样
std::shared_ptr<User> getUserById(int id);
// 或者
User getUserById(int id); // 值拷贝,简单安全
最后一个小技巧:
返回值里附带一个 requestId 或 traceId。当用户遇到问题时,可以拿着这个ID找你们的技术支持排查。这种设计在线上非常有用——你想想看,用户报个问题说“接口返回错误了”,你根本不知道怎么查。但如果用户告诉你“traceId 是 abc123”,你一秒就能定位到日志。
我个人所有SDK的返回结构里,都留了这个字段。效果非常好。
好了,接口设计规范这块就讲到这。命名清晰、参数精简、返回值明确,这三条做到了,你的SDK就成功了一大半。剩下的,就是保持一致性——让每个接口的使用体验都一样,用户用过一个就能举一反三。