3. 算法模块的接口契约设计与版本兼容

算法模块的接口契约设计与版本兼容 学习卡片

为什么算法模块需要接口契约

算法模块被上下游调用时,真正稳定的不是内部实现,而是输入输出接口、异常语义和版本兼容策略。如果只靠口头约定,内部一次重构(例如把返回的 list 改成 numpy.ndarray、把缺失值从 None 改成 NaN)就可能让调用方静默出错。接口契约把这些约定固化成可测试的断言。

算法模块接口契约:输入 → 处理 → 输出 → 异常 → 版本 上游调用方 predict(df) → result 输入契约 字段名 / 类型 / 形状 缺失值约定 / 取值范围 必填 vs 可选 算法核心 内部实现可重构 不暴露内部状态 纯函数优先 输出契约 返回类型 / 字段顺序 概率 vs 标签 / 置信度 空结果表示方式 异常语义(契约的一部分) • 输入字段缺失 → ValueError("missing: col") • 类型不符 → TypeError("expect numeric") • 空输入 → 返回空结果,不抛异常 版本兼容策略 • 新增可选参数:向后兼容(minor) • 删除/重命名字段:破坏性(major) • 弃用字段保留一个版本并告警 契约测试:上下游调用不因内部重构而破坏 ① 输入形状测试:给定合法 df,断言返回类型与字段 ② 异常测试:给定缺字段 df,断言抛出 ValueError 且消息含字段名 ③ 兼容测试:旧调用签名仍可运行,新参数有默认值 ④ 回归测试:固定输入 → 固定输出快照,重构后比对

接口契约的四个要素

一个可复用的算法模块,接口契约至少覆盖以下四类约定,缺一不可:

  • 输入契约:字段名、数据类型、形状、缺失值表示、取值范围、必填与可选参数。
  • 输出契约:返回类型(dict/DataFrame/ndarray)、字段顺序、概率与标签的区分、空结果的表示。
  • 异常语义:什么情况抛异常、抛什么类型、消息里包含什么信息;什么情况返回空结果而不抛异常。
  • 版本兼容:新增可选参数属于 minor 变更;删除或重命名字段属于 major 变更;弃用字段保留一个版本并给出告警。

用契约测试锁定接口

契约测试不验证算法精度,只验证接口行为是否与约定一致。下面给出一个最小可运行的契约测试示例,覆盖输入形状、异常语义和版本兼容三类断言。

# contract_test.py
import pandas as pd
import pytest
from algo_module import predict

def test_output_contract():
    """输出契约:返回 dict,含 label 和 score 两个键"""
    df = pd.DataFrame({"age": [25, 40], "income": [3000, 8000]})
    result = predict(df)
    assert isinstance(result, dict)
    assert set(result.keys()) == {"label", "score"}
    assert len(result["label"]) == len(df)

def test_missing_field_raises():
    """异常语义:缺字段抛 ValueError,消息含字段名"""
    df = pd.DataFrame({"age": [25]})
    with pytest.raises(ValueError, match="income"):
        predict(df)

def test_empty_input_returns_empty():
    """空输入返回空结果,不抛异常"""
    df = pd.DataFrame({"age": [], "income": []})
    result = predict(df)
    assert result["label"] == []

def test_backward_compatible_signature():
    """版本兼容:旧调用签名仍可运行"""
    df = pd.DataFrame({"age": [30], "income": [5000]})
    result = predict(df)  # 不传新参数
    assert "label" in result
契约测试应放在 CI 中,与单元测试分开运行。当内部实现重构时,只要契约测试通过,上下游调用方就不需要改动。

版本兼容策略速查

变更类型版本号变化调用方影响处理方式
新增可选参数(有默认值)minor +1无直接发布
新增返回字段minor +1无(调用方按需读取)文档标注
删除或重命名字段major +1破坏性保留旧字段一个版本并告警
修改异常类型major +1破坏性同时抛新旧异常或提前通知
修改默认值minor +1可能改变行为在 release note 中说明
不要用“内部重构”作为修改接口的理由。接口一旦发布,就应视为对外承诺;重构只能改变实现,不能改变契约。若必须破坏契约,走 major 版本并给调用方迁移窗口。