CLASS 05 · 2026-02-18 · 规格与设计

设计规格说明

不是所有规格都有用。本讲学习如何设计出清晰、恰当的规格:区分声明式与操作式、理解规格的强弱、掌握前置与后置条件的工程取舍,并认识可变性带来的风险。

确定规格 vs. 欠确定规格

如果一个规格对同一输入允许不同的结果,则称为欠确定(underdetermined)。完全确定的规格每次调用都唯一决定结果;欠确定规格则给实现者留出了自由度(例如排序算法可以返回任意一种稳定排序结果)。

声明式规格 vs. 操作式规格

声明式规格几乎总是更可取的。

声明式规格更简洁、更易于理解,且给实现者更大的优化空间。

// 操作式(不好):描述步骤
// "遍历数组,把每个元素逐个添加到一个新数组中..."

// 声明式(好):描述结果
/**
 * 返回数组的一个副本,其中元素按升序排列。
 * @param arr 输入数组
 * @returns 新数组,包含 arr 的所有元素且按升序排列
 */
function sort(arr: number[]): number[];

更强 vs. 更弱的规格

S2 比 S1 更强(stronger),当且仅当满足 S2 的实现集合是满足 S1 的实现集合的真子集。等价地说:S2 的前置条件弱于或等于 S1,且 S2 的后置条件强于或等于 S1。

好的规格应该对调用者足够强(提供有用的保证),同时对实现者足够弱(留出实现自由度)。

// 弱规格:只要求返回数组中的某个元素
// @returns arr 中的某个元素

// 中等规格:返回最大值
// @returns arr 中的最大元素

// 强规格:返回最大值及其索引
// @returns { value: arr 中的最大元素, index: 其索引 }

用图表示规格

把「所有可能的实现」看作一个空间,一个规格就定义了这个空间中的一个区域——区域内所有实现都满足该规格。更强的规格对应更小的区域。

可变性的风险

可变性(mutability)是许多 bug 的根源。两个典型风险:

别名是可变类型风险的根源

当多个引用(别名,alias)指向同一个可变对象时,任何一个引用都可能修改该对象,而其他引用浑然不觉。这就是可变类型危险的本质。

TypeScript 中的只读集合

TypeScript 提供了只读类型来防御修改:ReadonlyArrayReadonlySetReadonlyMap。它们省略了所有修改操作,让编译器替你守住不变性。

// 返回只读视图,防止调用者修改内部状态
function getItems(): ReadonlyArray<string> {
  return ["a", "b", "c"];
}

const items = getItems();
// items.push("d"); // Error: Property 'push' does not exist on type 'readonly string[]'.

设计好规格的原则

前置条件还是后置条件

是否使用前置条件是一个工程判断。前置条件把验证负担推给调用者(更弱的前置 = 更友好的 API);后置条件把验证负担留给实现者(更强的后置 = 更多的检查)。选择取决于错误的代价和验证的成本。

规格用在哪里

规格不仅用于公开 API,也用于辅助函数、导出函数、库函数和语言标准。任何需要他人(或未来的自己)理解的代码,都值得配上规格。

核心要点

声明式描述「做什么」而非「怎么做」;声明式规格更优。

强弱弱前置(对调用者友好)+ 强后置(对调用者有用)= 好规格。

可变性别名 + 可变对象 = bug;用只读类型防御。