契约、错误与异常
复习
- 分解、模块与接口:模块通过接口交流
- 异常处理:错误跨调用传播
- 类型系统:约束合法操作
TL;DR
- 每个函数或模块,都有一份“契约”:前提与承诺
- 输入不符合预期时,应当明确地失败
- 错误有不同类型:调用方错误、可恢复、不可恢复
- 明确失败,远好过悄悄出错
正文
模块之间通过接口交流。可接口上说的,不只是“函数叫什么、参数是什么类型”,还有一份更深层的约定——契约(contract)。
契约:前提与承诺
一份契约,通常包含:
- 前置条件(前提):调用方必须满足什么(比如“这个参数不能为负”)
- 后置条件(承诺):只要前提满足,被调方保证做到什么
- 不变式:在整个过程中始终成立的性质
可以把函数看成一份“合同”:你按前提调用我,我按承诺给你结果。 双方各守其责,合作才顺利。
前提不满足时怎么办
现实里,调用方未必守约——传了非法参数、给了坏数据。此时,被调方不能装作没事,而应明确地失败。
具体怎么做,取决于错误的性质:
- 调用方的错误:比如参数非法。通常应当明确报错(抛出异常或返回错误),让问题立刻暴露
- 可恢复的错误:比如文件暂时打不开。可以返回错误,让调用方决定重试或走别的路
- 不可恢复的错误:比如内存耗尽。可能只能终止,但要尽量留下信息
关键原则是:错误要被明确地处理,而不是被悄悄吞掉。
为什么“明确失败”更重要
有两类糟糕的做法,都和“不明确”有关:
- 悄悄出错:出了问题却装作正常,返回一个看似合理其实是错的结果。调用方毫不知情,错误会一路扩散,最后在一个离得很远的地方爆发,极难排查
- 吞掉错误:捕获了异常却什么都不做。问题被“藏”起来,下次再犯还是查不出原因
让错误尽早、明确地暴露,比让它悄悄蔓延安全得多。 这也呼应了异常处理的思路:错误的代价,往往不在于它发生了,而在于它被掩盖了。
思考题 1
一个函数的“契约”,通常包括什么?
思考题 2
为什么“悄悄出错”,比“明确失败”更危险?
小结
知识点
- 契约含前置条件、后置条件与不变式
- 输入不符合预期时应当明确失败
- 错误可分为调用方错误、可恢复与不可恢复
- 明确失败优于悄悄出错或吞掉错误
参考资料
- Wikipedia(zh):契约式设计:以契约描述模块的责任
- Wikipedia(zh):异常处理:明确处理错误的方式
思考题答案(仅供参考)
思考题 1
通常包括前置条件(调用方必须满足的约束)、后置条件(满足前提后被调方保证的结果),有的还包含不变式(过程中始终成立的性质)。它们共同约定双方的责任。
思考题 2
因为悄悄出错会让调用方以为一切正常,错误随之向远处扩散,最终在一个不相干的地方爆发,极难定位;而明确失败能让问题在发生处立刻暴露,便于及时发现和处理。被掩盖的错误,代价往往更大。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪