跳转到内容

引入断言

引入断言(Introduce Assertion)把代码默默依赖的一个假设写成断言:假设成立时什么也不发生,不成立时立刻报错。 它解决的痛是:某段代码只有在“折扣率不会是负数”这类前提下才正确,可这个前提只存在于写代码的人脑子里,读的人看不到,违反了也没人知道。

小何在写会员折扣。apply_discount 用折扣率算折后价,折扣率为负时会把价格算得比原价还高。按业务规则折扣率不可能是负数,可是这条规则没写在任何地方。某天运营后台的一个导入脚本把”-0.1”写进了数据库,顾客结账时多付了一成,过了一周才有人发现。

class Customer:
def __init__(self, discount_rate: float) -> None:
self.discount_rate = discount_rate
def apply_discount(self, amount: float) -> float:
if not self.discount_rate:
return amount
return amount - self.discount_rate * amount
print(f"{Customer(0.1).apply_discount(200):.2f}")
print(f"{Customer(0).apply_discount(200):.2f}")
# 输出:
# 180.00
# 200.00
class Customer:
def __init__(self, discount_rate: float) -> None:
assert discount_rate >= 0, "折扣率不能为负"
self.discount_rate = discount_rate
def apply_discount(self, amount: float) -> float:
if not self.discount_rate:
return amount
return amount - self.discount_rate * amount
print(f"{Customer(0.1).apply_discount(200):.2f}")
print(f"{Customer(0).apply_discount(200):.2f}")
# 输出:
# 180.00
# 200.00
class Customer {
constructor(readonly discountRate: number) {}
applyDiscount(amount: number): number {
return this.discountRate ? amount - this.discountRate * amount : amount;
}
}
console.log(new Customer(0.1).applyDiscount(200).toFixed(2));
console.log(new Customer(0).applyDiscount(200).toFixed(2));
// 输出:
// 180.00
// 200.00
function assert(condition: boolean, message: string): asserts condition {
if (!condition) throw new Error(message);
}
class Customer {
constructor(readonly discountRate: number) {
assert(discountRate >= 0, "折扣率不能为负");
}
applyDiscount(amount: number): number {
return this.discountRate ? amount - this.discountRate * amount : amount;
}
}
console.log(new Customer(0.1).applyDiscount(200).toFixed(2));
console.log(new Customer(0).applyDiscount(200).toFixed(2));
// 输出:
// 180.00
// 200.00

合法数据下输出完全一样,断言不改变正确程序的行为。区别在于有人造出负折扣率的那一刻:重构前它会一路走到结账,算出一个错的价格;重构后在构造 Customer 时就报错,报错位置指向导入脚本。断言放在构造函数而不是 apply_discount 里,就是为了让错误在源头暴露。TS 没有内置的 assert 语句,这里自己写了一个,asserts condition 这个返回类型还能让编译器在断言之后收窄类型;Node 环境下也可以用标准库的 node:assert。

引入断言:用断言明确表达代码对程序状态所做的假设。

断言写的是“如果这里不成立,就是程序员犯了错”,它是给读代码的人看的文档,也是给测试的报警器。适合写断言的地方是:一段代码只有在某个前提下才正确,前提由程序内部保证而不是由用户输入决定,比如“折扣率非负”“列表已经排好序”“这个字段到这里一定已经被赋值”。断言失败意味着有 bug,应当去修代码,而不是去处理这个情况。

断言不是输入校验。用户填的表单、接口传来的参数、文件里读出的数据,本来就可能不合法,要用普通的 if 加异常或错误响应来处理,并给出用户能看懂的提示。Python 用 -O 参数运行时会把 assert 语句全部去掉,靠 assert 挡住的非法输入在生产环境就直接放过去了。本例中折扣率来自运营后台,如果后台允许人工录入,正确的做法是在录入时校验,构造函数里的断言只是最后一道防线。另外,别给每一个显而易见的事实都加断言,assert len(items) >= 0 只是噪音,断言要留给那些“违反了会悄悄算错”的假设。