让 bug 越早暴露越好 (1)

757 查看

不管你的水平多高,多么认真仔细,程序员总是会制造出 bug。bug 的根本来源是:

  1. 预期之外的用户输入或系统输入。典型的缓冲区越界、SQL 注入等攻击手段,或者网络异常等。它的特点是在通常的用户输入时,程序表现得非常正常;但对于不普通、或者异常的用户输入没有做任何防御。
  2. 程序员的疏忽或理解错误。例如不正确地调用了参数,按照不正确地次序调用函数,错误的线程数据共享。
  3. 开发组件之外的系统其他部分,如中间件、数据库系统或其他组件本身的 bug,也会给我们本来完美的程序带来 bug。但这种 bug 我们可以称为是衍生错误,本质是上述两种 bug 之一。

所谓防御性编程(Defensive Programming) 一般针对第一类错误,它的原则是:只要有用户或系统输入,就应该对它的合规性进行严格的检查。例如:

javapublic String findUserName(String userId) {
  return db.userNameFrom(db, userId);
}

如果 nameInput 是直接来自于用户输入(不论是通过页面,还是 URL,还是应用程序),上述简单程序意味着:只要 userId 是一个字符串,它就是合法的!而我们就将它作为合法字符串在系统各处传递。这假设实际上往往是错误的,绝大多数系统实际对用户信息的要求不仅仅是满足基本类型信息即可,而有显式或隐式的其他条件。例如,userId 很可能有长度限制,例如长于 6 个,短于20个字符;它很可能有取值限制,例如只能是字符和数字。而我们的编译器,即使是 Java 的强类型编译器,对这种要求一无所知。是程序员的责任来检查这些额外要求:

java//一个特别的类 InputChecker 来检查输入
public void checkUserId(String userIdInput) {
  if (userIdInput.length < 6 || userIdInput.length > 20)
    throw new InvalidInputException("userId", userIdInput);
}

//需要和用户输入直接打交道的地方调用 inputChecker
public String findUserName(String userId) {
  inputChecker.checkUserId(userId);
  return db.userNameFrom(db, userId);
}

注意,由于异常输入是一种异常,用运行时异常来表达它是合理的。当然对于这种可能出现的异常应该在程序的其他地方捕获并加以恰当的展示。

这样的防御性编程代码是可靠设计的一个良好习惯,实际上它也广泛地被使用。但也有很多程序员将它错误地过多使用,或者错误地用它来覆盖第二类 bug (程序员错误)。

javapublic List<int[]> transferGameResult(GameRoom room, List<int[]> gameResult) {
 if (null == gameResult || gameResult.size() == 0) {
    return null;
 .....
}

这段程序的目的是将一种游戏结果转化为另一种表达式来方便计算。其中第一句语句的目的是?

看起来这似乎是防御性编程,想要检查 gameResult 的输入错误:如果 gameResult 不正确输入,就返回空值。

但这里的 gameResult 是用户输入吗?不可能是,它是一个 int[],只可能是来自于程序其他地方,也就是说来自于这段代码的调用者,另一个程序员。

用正确的参数来调用是程序员的责任,我们不能假装我们的程序可以正常处理 null 的参数或者长度为0 的输入,实际上对此我们没有处理能力。因此,这里真正防御性编程的处理是:

javapublic List<int[]> transferGameResult(GameRoom room, List<int[]> gameResult) {
   assert(gameResult != null && gameResult.size() > 0);
 .....
}

表明我们的这段程序对输入参数的严格要求。有人可能会说,assert 不是不应该出现在运行时的系统中吗?至少对于 Java 语言来说,assert 将立即抛出一个异常(如果你没有停用 assert 的话),调用程序员如果对引用这段代码的程序做了合理的测试,他会立即发现这个错误,就有了纠正它的机会。但按照之前的写法,调用程序员则很可能不能看到任何问题,而使用不正确的参数调用,本身就是一个错误!这样,我们成功地帮助程序员掩盖了他的错误,增加了将 bug 带到生产系统中的机会。