摘要
本文主要介绍了两种编程风格:契约式编程/进攻式编程,防御式编程
- 契约式编程使用契约来约束函数的调用者和被调用者,如果不履行义务,程序就可以不执行
- 防御式编程假定人都会犯错,并且对错误给予很大程度的容忍
这两种编程风格会被混合使用,应对不同的情形
异常处理
程序中的异常大致可以分为两类:
- 一类是程序员引发的异常,就是
BUG
,例如:参数传递不合理,逻辑错误 - 一类是运行时的异常
进攻式编程和方式式编程的思维差异:
- 进攻式编程:主动暴露可能出现的错误,让它在开发阶段显露出来。
- 防御式编程:不能确定主调函数会传进来什么样的参数,无论什么参数都能工作。
契约式编程和防御式编程,是两种不同的思路,两种之间存在着一些冲突。
断言是基于假设(契约)的,用来检查不应该发生的情况,如果发生了,那就是BUG,是程序员的错;错误处理是用来检查可能会发生的异常情况的,一般用来处理复杂的外部情况。
契约:可以区分程序员引发的异常和运行时的异常。(难点在于制定契约)一般来说,对于内部函数,契约严格;对于外部函数,契约宽松,配合防御式编程
对于程序员引发的异常,使用进攻式编程;
对于运行时错误,使用防御式编程;
一定要区分程序员异常和运行时异常,否则会埋下隐患:
const JSON *json_find_member(const JSON *json, const char *name)
{
if (!json)
return NULL;
if (json->type != JSON_OBJ)
return NULL;
if (!name)
return NULL;
...
}
契约式编程
契约:调用者和使用者应该遵守的约定,一般使用 assert
来检查这些约定。使用契约式编程可以简化异常处理的逻辑。
契约的分类
- 前置条件:在函数调用之前需要满足的条件,
BUG
责任人是函数调用者。可以通过assert
判断参数的方式来检查前置契约 - 后置条件:函数在返回之前,必须满足的条件,
BUG
责任人是函数实现者。可以通过assert
判断返回值的方式来检查后置契约 - 部变量:应该存在的关系。比如对于一下结构体,契约规定:当
size > 0
时,应该有buf != NULL
。
// 不变量的示例
struct buf_t{
size_t size;
char* buf;
};
进攻式编程:违反契约的规定就意味着 BUG
。进攻式编程就是:尽量使 BUG
在开发阶段就显现出来。
进攻式编程的做法:尽量全面地检查契约,以明显的方式(程序退出,日志报告)暴露出来
防御式编程
防御式编程用于应对运行时异常。一般通过 if
判断的形式来进行防御式编程。
一般来说,防御式编程会配合比较宽松的契约条件。
过度的防御会造成异常处理模块复杂。
区分契约(进攻)和防御式的规定:
进攻式:对参数十分严格,职责明确
防御式:对参数容忍较高
// 契约式的
JSON* json_add_member(JSON* json, const char* key, JSON* val)
{
assert(json);
assert(json->type == JSON_OBJ);
assert(key);
assert(key[0]);
}
// 防御式的
JSON* json_add_member(JSON* json, const char* key, JSON* val)
{
if (!json || json->type != JSON_OBJ)
return NULL;
if (!key || !key[0])
return NULL;
}
错误处理的方法,按照是否影响程序运行,可以分为两类:
存在多种日志,不同的日志面向的人不同,打印的信息不同,打印的频率不同,作用也不同
日志种类 | 面向人群 | 作用 | 何时打印 | 精确级别 |
---|---|---|---|---|
实时调试日志 | 开发人员 | 了解程序内部运行状态,给出运行的路径信息 | 重要的数据被修改时;出现错误的第一时间;消息处理函数的时候; | 函数行数 |
调试日志 | 技术支持 | 帮助了解系统发生的关键活动,方便定位问题 | 一些关键的阶段才打印 | 函数行数 |
信息日志 | 用户 | 告知系统发生过的重要事件 | 哪些机制被开启了,启用了哪些参数等 | 模块 |
告警日志 | 管理员 | 预警,告知异常状态,以及处理措施 | 系统处于不正常状态时,但还可以运行 | 模块 |
可以参考的错误处理机制:
- 底层函数检测到错误,打印调试信息,并且通过返回值或异常将错误返回给上层
- 上层在业务层进行错误处理,输出给用户,或者吞掉
注:底层最适合打印调试信息了。可以将常用的标准 API
给封装一下,减少打字,也方便修改。