迁移基础 · 工程边界 · LESSON 11

API 边界与契约

用输入、输出、不变量和失败语义定义稳定接口,而不是只传一堆对象。

14 分钟api · contracts · invariants

API 边界与契约

TS interface 能描述字段,却不会自动保证运行时输入可信。Python 和 C++ 同样需要在边界校验数据,并决定错误通过异常、返回值还是状态对象表达。一个 API 契约不仅是字段表,还要写清必填字段、单位、范围、默认值、幂等性、超时和失败语义。这样 Web、Python 推理服务和 C++ 机器人节点才能独立演进。

学习目标

  • 能区分编译期类型提示与外部数据的运行时可信度。
  • 能为字段写出必填条件、范围、单位和失败类别,并在系统边界集中校验。
  • 能判断输入错误、依赖超时和服务端故障各自是否可恢复、是否适合重试。

接口首先要说明失败怎么发生

下面这段代码只保留同一个意图,重点观察输入边界、数据流和失败语义,而不是逐字符翻译。

TRANSLATION LENS 同一个意图,两种工程表达 窄屏可左右滑动查看完整代码
JS / TS
type Prediction = { label: string; score: number };
function accept(prediction: Prediction) {
if (prediction.score < 0) throw new Error("invalid");
}
Python / C++
# Python
def accept(prediction: Prediction) -> None:
  if not 0 <= prediction.score <= 1:
      raise ValueError("score must be in [0, 1]")

// C++
bool accept(const Prediction& prediction) {
return prediction.score >= 0.0 && prediction.score <= 1.0;
}

从类型提示到运行时契约

类型检查和运行时校验解决的是不同问题:前者帮助开发者,后者保护系统免受网络、文件和设备输入影响。解析 JSON 时应先判断字段存在和基本类型,再检查业务范围;不要让内部函数每次都猜测 score 是否可能为字符串。对时间、角度、坐标系和置信度的单位写进契约,尤其要避免“数字看起来一样”造成静默错误。

def parse_prediction(raw: dict) -> tuple[str, float]:
    label = raw.get("label")
    score = raw.get("score")
    if not isinstance(label, str) or not label:
        raise ValueError("label is required")
    if isinstance(score, bool) or not isinstance(score, (int, float)):
        raise ValueError("score must be numeric")
    if not 0.0 <= float(score) <= 1.0:
        raise ValueError("score must be in [0, 1]")
    return label, float(score)

设计稳定的失败语义

区分客户端输入错误、认证失败、限流、依赖不可用和服务内部错误。HTTP 可以用状态码与错误码,CLI 可以用退出码,C++ 库可以返回 expected 或状态对象;重要的是调用方能稳定判断是否重试。契约测试应锁住字段名、范围和错误码,而不是只断言一段自然语言消息。

常见错误与排错思路

常见错误是只在 TypeScript 编译期写 Prediction,然后直接信任 JSON.parse;另一个是把所有异常都返回 500,让调用方无法修复输入。排错时保存请求 id、schema 版本和失败字段名,比较进入边界时的原始类型与转换后的内部值;若分布式组件行为不一致,先对照契约和实际 payload,再看算法。

逐步建立可信输入

先接收宽松的外部对象,再把它转换为核心使用的规范值。转换函数应只做边界检查;模型推理、数据库写入等副作用留给后续层。下面的例子定义了一个可观察的结果,而不是依赖调用方猜测异常。

function parseScore(input) {
  if (typeof input?.score !== "number" || !Number.isFinite(input.score)) {
    return { ok: false, code: "invalid_score" };
  }
  if (input.score < 0 || input.score > 1) {
    return { ok: false, code: "score_out_of_range" };
  }
  return { ok: true, score: input.score };
}

console.log([{}, { score: 1.2 }, { score: 0.7 }].map(parseScore));

预期是依次得到 invalid_scorescore_out_of_range 和成功值 0.7。移到 Python 时可用带类型的结果或自定义异常;C++ 可用 std::expected(标准库支持时)或明确的结果结构。无论语法如何,调用方都应能区分相同的失败类别。

验证与排错

为每个字段建立边界表:缺失、类型错误、最小合法值、最大合法值、超范围,以及合法常规值。逐项提交到校验函数,确认错误码稳定;随后让调用方只对 dependency_timeout 做有限退避,而不重试 invalid_score。若三种语言出现不同结果,先比较同一份 JSON 样例、schema 版本和数值精度,再定位语言实现。

再把这些结果写进接口契约测试:固定一份输入与期望错误码,不能只检查“调用失败”。如果 HTTP 服务负责暴露此接口,还要验证错误类别到状态码的映射;CLI 或机器人节点则可能采用退出码或 fault 状态,但内部错误类别仍保持一致。契约测试最好由消费者也能运行,避免生产者单方面改变字段后才发现下游不兼容。

不要用“返回 400”替代契约:客户端仍需知道哪个字段无效;也不要把所有错误都返回 500,否则重试会放大本可由用户修复的问题。日志记录 request id、错误码和字段名,不记录敏感原文。

兼容性检查清单

每次修改契约前,逐项确认必填字段是否改变、字段单位是否改变、默认值是否安全、旧消息是否还能解析、失败是否可被消费者识别。对于可能导致行为变化的字段,增加 schema 版本或新字段并保留短期双读路径;不要复用同一字段名表达不同单位。验证结果应保存为正常消息与失败消息 fixture,作为三语言实现共同的回归标准。发布前让至少一个旧消费者读取新样例、让新消费者读取旧样例;把不支持的组合列为显式拒绝,而不是悄悄补默认值。

迁移练习

请完成:为一个 AI 置信度接口补充范围校验、错误信息和调用方处理方式,覆盖字段缺失、score 非数字、超时和服务端 5xx 四种情况。

01
TRY IT YOURSELF

API 边界与契约练习

为一个 AI 置信度接口补充范围校验、错误信息和调用方处理方式,覆盖字段缺失、score 非数字、超时和服务端 5xx 四种情况。

给我一点提示

把 score 的合法区间和缺失字段分别作为契约写出来,并区分可重试与不可重试。

查看参考答案
缺失字段和非数字返回 invalid-input,不重试;score 超出 0..1 也拒绝并指出字段;连接超时返回 dependency-timeout,可按上限重试;5xx 返回 dependency-unavailable,仅对幂等请求退避重试。
本节结论

跨语言迁移的第一项工程能力,是把隐含假设变成可以测试的接口契约。契约越具体,内部算法越能专注于业务,调用方也越容易在 Python 与 C++ 之间互操作。

小结

边界负责把不可信输入转换成满足约束的内部值,并提供稳定失败语义。进入下一课数据模型时,继续把本课的字段范围、单位和缺失规则写进模型构造过程。

当前学习阶段工程边界
0/8

阶段共 8 节课,按顺序完成更容易建立完整的迁移模型。