API and Interface Design(接口与边界设计)
Overview
好接口让对的事容易、错的事难。每个可观察行为都是潜在承诺:用户够多时,连 bug、报错文本、时序都会有人依赖。设计时就要决定暴露什么、怎么错、怎么变。
When to Use
- 定模块边界、公开函数、团队间契约时
- 新增 API endpoint、配置字段、事件格式时
- 统一错误语义(抛/返回/null 三选一)时
- 改公开签名、加必填项、做破坏性变更前
When NOT to use:
- 纯内部函数重构(无下游、无公开承诺)
- 一次性脚本
- 已有契约且本次不碰边界,只是内部实现
Hyrum's Law(设计前提)
用户够多时,一切可观察行为都会被依赖——包括未文档化的怪癖、报错文本、时序、顺序。所以:
- 有意暴露:每个公开项都是承诺,问一句"3 年后还愿意维护它吗"
- 不漏实现细节:能被观察到就会被依赖;日志格式、内部错误文本不进契约
- 设计时定废弃:怎么拆、迁移期多长,先写好(见
deprecation-and-migration) - 测试不够:契约测试全绿也拦不住依赖未文档行为的用户,"安全"改动也可能破下游
Contract First(先契约后实现)
// 先定契约:参数、返回值、错误码,冻结后再实现
// 例:main(argv) -> exitCode,内部归一,不直接退出进程
- 契约即 spec,实现跟着走;扩展只加可选,不改已有签名
- 扩展用对象可选字段,不用位置参数加参
- IO 与纯逻辑分开:纯函数保持纯输入输出,可测部分返回结果对象而非只打日志
边界划分
- 公开面最小:下游只调编排入口,拼装细节(解析/渲染/写盘/交互)全内部不公开
- 副作用隔离:混了日志/写盘的函数不公开;公开即把日志格式变成 API
- 生成物封装:自动生成文件由拥有者模块封装,下游不直引
- 构建契约同等:靠特殊机制(拼接/截断/生成)的行视同公开契约,改即 breaking
错误语义统一(三选一,全仓一致)
- 内部只抛,不返回 null/{error}:null 分不清失败与缺席,
{error}让每个调用方写 if 必然漏接 - 顶层一个出口归一:唯一入口
try/catch,返 exit code,内部不process.exit、不各自打印 - 预期失败 vs Bug:预期失败抛具名错误(code + 用户可行动提示 + hint);非预期让原始异常上抛 + 堆栈,不包装吞掉
- 禁混用:有的抛、有的返 null、有的返
{error}即违规,下游无法预测
边界校验(信内不信外)
- 校验在系统边:CLI 参数、API 路由、外部响应、环境变量加载处
- 校验完内部信任类型,不层层复验
- 第三方返回视为不可信:先验 shape 再进逻辑/渲染
- 内部函数之间、刚出自家 DB 的数据不重复校验
只加不改(兼容演进)
- 新字段一律可选 + 合理默认值,缺失自动填,不中断老用户
- 读取层
?? default,缺失不报错;非法值才报错 - 老配置回归测试:不带新字段跑全流程,全绿才合
- 转必填等下个 major,提前文档 + CHANGELOG 声明废弃期
- One-Version:不逼用户二选一,扩展不分叉
命名可预测
| 场景 | 约定 | 例 |
|---|---|---|
| 公开函数 | 动词 + 对象 | runInstall, createTask |
| 布尔 | is/has/can 前缀 | isForced, hasConfig |
| 错误码 | UPPER_SNAKE | TARGET_NOT_EMPTY, NOT_FOUND |
| 配置字段 | camelCase(或本仓约定) | targetDir, dryRun |
Quick Reference
| 场景 | 动作 |
|---|---|
| 定边界 | 公开面最小,副作用不公开,生成物有人封装 |
| 定错误 | 内部抛具名错误,顶层归一 exit code,不混 null/{error} |
| 加字段 | 可选 + 默认 + 老配置回归绿,major 才收紧 |
| 改签名 | 先废弃别名 + 迁移期(见 deprecation),不直改 |
| 接外部数据 | 先验 shape,第三方返回当不可信 |
Common Rationalizations
| Excuse | Reality |
|---|---|
| "多公开几个方便测试" | 测试直引内部函数即把实现锁成契约;可测性靠纯函数 + 结果对象,不是公开一切 |
| "返回 null 简单" | null 语义模糊,调用方必漏接;具名错误 + 顶层归一才是简单 |
| "加个必填字段而已" | 必填即 breaking;可选 + 默认 + 迁移期,否则等 major |
| "报错文本随便写" | Hyrum's Law:文本会被依赖;code 稳定,message 可读,细节放 hint |
| "内部校验一层更保险" | 层层复验即噪音;边界验完内部信类型,保险靠测试不是复验 |
Red Flags — STOP
- 公开面含拼装细节/副作用函数/生成物直引
- 错误语义三混(抛 + null +
{error}并存) - 新必填字段无迁移期
- 公开改名无别名直接改
- 第三方返回未验即用
- 报错文本当契约依赖(下游正则匹配 message)
以上任一出现 → 停手,回契约/边界修正后再合。
Verification
- 公开面最小且有契约(签名/返回值/错误码)
- 错误语义全仓统一,顶层归一
- 边界有校验,内部无复验,第三方返回已验
- 新增全可选 + 默认,老配置回归绿
- 命名符合约定,错误码 UPPER_SNAKE