把问题写成可检查的规则

程序接收输入,按照规则改变状态,产生输出。先写清输入允许什么、输出保证什么,比直接选语言更有用。例如库存扣减必须保证库存非负;仅仅让函数返回一个数字,还没有表达完整需求。

状态可以是变量、文件、数据库记录,也可以是外部设备的位置。纯函数只依赖参数并返回结果,容易重复检查;涉及网络、时间和设备的操作需要另外处理失败与重试。

一个库存例子

def reserve(stock, amount):
    if not isinstance(stock, int) or isinstance(stock, bool) or stock < 0:
        raise ValueError("invalid stock")
    if not isinstance(amount, int) or isinstance(amount, bool) or amount <= 0:
        raise ValueError("invalid amount")
    if amount > stock:
        raise ValueError("not enough stock")
    return stock - amount

assert reserve(8, 3) == 5

这里的前置条件是库存非负、数量为正整数;后置条件是返回值等于原库存减数量,且仍非负。函数本身没有修改数据库,因此两个人同时下单时,还需要数据库事务或原子更新来维护同一规则。

不变量是在指定执行阶段始终成立的条件。循环处理一组订单时,可以规定“已处理订单的总扣减量等于原库存与当前库存的差”。初始化、每次迭代和退出时分别检查,能帮助发现漏减和重复扣减。

变量与控制流

变量绑定值,分支依据条件选择路径,循环重复处理一类状态。函数把一个可命名的任务封装起来。类型说明值可以怎样使用;它不能代替业务条件,例如整数类型本身并不保证数量大于零。

从顺序代码走向事件、并发和消息系统后,执行顺序更难预测。先用小规模顺序实现验证规则,再明确哪些状态可以共享、哪些操作必须原子完成。

练习与面试自查

给 reserve 增加边界检查:扣完库存、超额扣减、零、负数和布尔值。分别写出预期结果。再解释为什么两个请求都先读取库存 8、各扣减 5,可能通过函数检查却破坏系统规则。

检查一个函数时,应能说清输入、输出、副作用、前后条件和失败方式,而不只是逐行描述语法。

参考资料