摘要

本文主要介绍了两种编程风格:契约式编程/进攻式编程,防御式编程

  • 契约式编程使用契约来约束函数的调用者和被调用者,如果不履行义务,程序就可以不执行
  • 防御式编程假定人都会犯错,并且对错误给予很大程度的容忍

这两种编程风格会被混合使用,应对不同的情形

异常处理

程序中的异常大致可以分为两类:

  • 一类是程序员引发的异常,就是 BUG,例如:参数传递不合理,逻辑错误
  • 一类是运行时的异常

进攻式编程和方式式编程的思维差异:

  • 进攻式编程:主动暴露可能出现的错误,让它在开发阶段显露出来。
  • 防御式编程:不能确定主调函数会传进来什么样的参数,无论什么参数都能工作。

契约式编程和防御式编程,是两种不同的思路,两种之间存在着一些冲突。

断言是基于假设(契约)的,用来检查不应该发生的情况,如果发生了,那就是BUG,是程序员的错;错误处理是用来检查可能会发生的异常情况的,一般用来处理复杂的外部情况。

契约:可以区分程序员引发的异常和运行时的异常。(难点在于制定契约)一般来说,对于内部函数,契约严格;对于外部函数,契约宽松,配合防御式编程

对于程序员引发的异常,使用进攻式编程;

对于运行时错误,使用防御式编程;

一定要区分程序员异常和运行时异常,否则会埋下隐患:

  1. const JSON *json_find_member(const JSON *json, const char *name)
  2. {
  3. if (!json)
  4. return NULL;
  5. if (json->type != JSON_OBJ)
  6. return NULL;
  7. if (!name)
  8. return NULL;
  9. ...
  10. }

契约式编程

契约:调用者和使用者应该遵守的约定,一般使用 assert 来检查这些约定。使用契约式编程可以简化异常处理的逻辑。

契约的分类

  • 前置条件:在函数调用之前需要满足的条件,BUG 责任人是函数调用者。可以通过 assert 判断参数的方式来检查前置契约
  • 后置条件:函数在返回之前,必须满足的条件,BUG 责任人是函数实现者。可以通过 assert 判断返回值的方式来检查后置契约
  • 部变量:应该存在的关系。比如对于一下结构体,契约规定:当 size > 0 时,应该有 buf != NULL
  1. // 不变量的示例
  2. struct buf_t{
  3. size_t size;
  4. char* buf;
  5. };

进攻式编程:违反契约的规定就意味着 BUG。进攻式编程就是:尽量使 BUG 在开发阶段就显现出来。

进攻式编程的做法:尽量全面地检查契约,以明显的方式(程序退出,日志报告)暴露出来

防御式编程

防御式编程用于应对运行时异常。一般通过 if 判断的形式来进行防御式编程。

一般来说,防御式编程会配合比较宽松的契约条件。

过度的防御会造成异常处理模块复杂。

区分契约(进攻)和防御式的规定:

进攻式:对参数十分严格,职责明确

防御式:对参数容忍较高

  1. // 契约式的
  2. JSON* json_add_member(JSON* json, const char* key, JSON* val)
  3. {
  4. assert(json);
  5. assert(json->type == JSON_OBJ);
  6. assert(key);
  7. assert(key[0]);
  8. }
  9. // 防御式的
  10. JSON* json_add_member(JSON* json, const char* key, JSON* val)
  11. {
  12. if (!json || json->type != JSON_OBJ)
  13. return NULL;
  14. if (!key || !key[0])
  15. return NULL;
  16. }

错误处理的方法,按照是否影响程序运行,可以分为两类:

  • 影响程序运行,主要是:返回值,异常
  • 不影响程序运行,主要是:日志

    日志

存在多种日志,不同的日志面向的人不同,打印的信息不同,打印的频率不同,作用也不同

日志种类 面向人群 作用 何时打印 精确级别
实时调试日志 开发人员 了解程序内部运行状态,给出运行的路径信息 重要的数据被修改时;出现错误的第一时间;消息处理函数的时候; 函数行数
调试日志 技术支持 帮助了解系统发生的关键活动,方便定位问题 一些关键的阶段才打印 函数行数
信息日志 用户 告知系统发生过的重要事件 哪些机制被开启了,启用了哪些参数等 模块
告警日志 管理员 预警,告知异常状态,以及处理措施 系统处于不正常状态时,但还可以运行 模块

可以参考的错误处理机制:

  • 底层函数检测到错误,打印调试信息,并且通过返回值或异常将错误返回给上层
  • 上层在业务层进行错误处理,输出给用户,或者吞掉

注:底层最适合打印调试信息了。可以将常用的标准 API 给封装一下,减少打字,也方便修改。