[译] 让错误的代码更容易被发现

关键词:代码整洁,更好的暴露错误,对匈牙利命名法的误解,慎用异常
 
     在一个极度无聊的周末,我拿出角落里的《游戏引擎架构》这本书,打开扉页我发现赫然写着:购于2014年4月20日。这意味着我已经半年没有进行阅读了,简直不能忍,于是我开始了鲜有的周末学习之旅。老实说,使我真正来兴趣的是当读到关于编码标准一章,发现作者极力推荐Joel Spolsky的一篇文章《Making Wrong Code Look Wrong》的时候。这里我简要的总结一下作者的思想。
     作者以代码整洁的几种境界入手,向我们阐述了代码整洁的几个过程。在我们编程之初,我们多少会学习或者注意到了coding-style这么个东西,于是,开始纠结到底使用 One True Brace Style还是别的诸如K&R style,使用这种string类亦或者是另外一种string类,这时候的你处在第一个阶段:你并不知道clean和unclean的区别。当你能够更加熟练地编写代码后,你会注意到保持一种良好的代码习惯,而你在保持这种习惯之时会不知不觉犯各种错误,例如:
      char* dest, src;
这无疑是完全合法的语句,也符合你的编码习惯,然而声明一个指向char类型的指针dest和一个char字符src,这可能并不是你正真想要的结果。代码开始有点变质了。你开始进入第二个阶段:你对于代码整洁仅仅有着浅显的认识,更多地停留在遵循一致的编码规范上。
     当你面对下面的代码时,你开始察觉到了一些细微的变化:
     if (i != 0)
          foo(i);
代码正确无误!但你可能有稍许不安,万一谁想在foo函数之前插入另一个函数呢?
     if (i != 0)
          bar(i);
          foo(i);
碰巧又忘了加上花括号,问题就严重了。于是,你进入了第三个阶段:你不仅仅只注意到表面的,形式上的一些东西,你开始留意一些细微的,由代码不整洁带来的具有潜在可能性的bug。
     事实上,作者真正要表达的却是第四个阶段:你故意地构建你的代码,使得你能更好的嗅到代码中可能不正确的地方。
     接下来,作者由浅入深地给出了一些列子,还牵扯到了关于匈牙利命名法和异常机制的争辩。显然,作者的目的达到了,至少让我对于匈牙利命名法有了新的认识。
     对于Cross Site Scripting Vulnerability的例子,实在没有必要做复述,有兴趣的可以去看作者的原文。
     对于如何保持代码整洁,作者列举了一些通用的规则:
     ·保持函数的精简
     ·使变量的声明紧靠使用的地方
     ·不要使用宏来创建你自己的编程语言
     ·不要使用goto语句
     ·不要使成对的花括号距离超过一个屏幕
     这些规则都有一个共同点,那就是尽量把相关度高的代码放在靠近的位置,这能够使你更迅速地搞清楚代码到底是在干嘛。
     当你看到这样的代码:
     i = j * 5;
     如果是C语言,你会肯定地知道,j被乘了5被,然后赋给了变量i
     如果是C++,你什么也不会知道!唯一的办法是搞清楚i和j的类型,它们可能被定义在别的地方,因为j的类型可能重载了*操作符,这里面可能做了很多事情,而i的类型又可能重载了=操作符,还没有完,i和j可能不是同一个类型,所以又可能在最后发生强制类型转换。你会发现,光搞清楚i和j的类型还不够,得找到他们的实现代码(万一找不到你就只能呵呵了),庆幸的是,你终于找到了,然而你却发现这里面有继承关系,然后你又只能去找父类,如果里面用到了多态,抽象接口,你就真是遇到了麻烦,你还得知道i和j在运行时的类型...(这种情况确实很容易出现)
     再来看看匈牙利命名法,它原本是微软word项目的一名程序人员Simonyi提出的,简而言之,就是在所有的变量之前加上一个小写的前缀来表明该变量所包含的内容(In Simonyi’s version of Hungarian notation, every variable was prefixed with a lower case tag that indicated the kind of thing that the variable contained.)这里之所以使用kind一词,正是因为Simonyi在论文中误用了type一词,导致了数代程序员的误解。
     如果仔细阅读了Simonyi的论文,会发现,他所要表达的意思并非为变量加上表示类型的前缀,诸如iWidth,iHeight之类。Simonyi最初的观点在微软被称为Apps Hungarian,是站在应用程序的视角而言。在Excel的源码中你可以看到非常多的rw和col前缀的变量,非常清晰的代表了row和column,显然,他们都是整形的,并且,如果你在程序中发现他们之间有交换赋值的操作,显然就是错误的(这也是为什么作者一直强调让错误的代码更加容易地被发现)
     Apps Hungarian的前缀命名法在函数的命名中也得到应用。有理由相信在Word的源码中有一个函数YlFromYw表示垂直窗口坐标到垂直布局坐标的转换。Apps Hungarian倡导使用TypeFromType而非传统的TypeToType的方式命名函数,这样你在看到函数开头时就知道它的返回值所表示的内容。
     Apps Hungarian是非常有用的,例如‘ix’表示数组的索引,‘c’表示数量,‘d’表示两个数的差值(’dx‘表示’width‘)等等。
     然而一些错误的事情发生了,没有人知道原因,但似乎就是微软的文档撰写者无意中创造了后来为人诟病的Systems Hungarian。一些人读了Simonyi的文章之后,发现他使用了‘type’,便认为是语法意义中的类型,就像class一样。但事实上Simonyi表达的并非这个意思,尽管他非常小心地解释了type的含义。
     直到今天, “Do Not Use Hungarian Notation” 的声音还是出现在各个项目的编码规范文档中,对于Systems Hungarian而言,显然是没有什么问题的,但人们如果能够明白 Apps Hungarian,使用它,代码将更好阅读,调试,维护,使得错误的代码更为容易被发现。
     作者对匈牙利命名法的观点大致止于此。对他的观点,我个人也深有感触,为什么自己一个月之前写的代码都看不懂?多半是因为没有形成好的命名习惯。有时候我们为了快速地完成功能,很少去对命名做仔细的斟酌思考,而是随手写一个变量或者函数(诸如flag这种变量),给变量或函数起一个好名字确实需要耗费我们的时间,但是换来的确实良好的可读性,有其是在团队开发中,遵循一致的风格能够减少大量沟通成本,这一点时间的耗费完全值得。
     最后,作者谈及了异常机制。作者认为大多数时候异常给程序猿带来的麻烦远比好处要多,异常实际上是非显试的goto,他还提到了一位和它具有同样观点并且他认为是世界上最好的程序员,Remond Chen(google了一下,是微软的一名员工,著有《windows编程启示录》一书,似乎不太出名,不明白作者为什么称之为世界上最好的程序员)。当你看到如下的代码:
     dosomething();
     cleanup();
     有什么问题呢?cleanup总会被调用啊!然而都something()如果抛出了一个异常,cleanup就可能不会被调用。当然,使用finally或者其他的机制可以保证cleanup被调用,但重点是:你为了找出cleanup之前是不是会出发一个异常,你不得不查看dosomething的整个调用树。你为了找到当前问题的答案必须去查看其他的地方,甚至完全不相干的地方。错误的代码很难被找出。
     不过,对于脚本或者一些数据处理,打印小程序而言,异常还是非常有用的,用一个try catch来忽略掉不重要的错误让脚本更小更简单。异常对于那些quick-and-dirty 的代码,脚本是非常好的。不过如果你在写一个操作系统,一个核电站程序,或者用于心脏手术的高速锯齿控制程序,异常就非常危险了。
     The way to write really reliable code is to try to use simple tools that take into account typical human frailty, not complex tools with hidden side effects and leaky abstractions that assume an infallible programmer.真正具有可靠性的代码原则是使用简单的机制来防止人为的疏漏;而不是使用复杂的,具有隐藏的副作用的高级机制和有漏洞的抽象让程序员去保证其完全正确无误。(关于leaky abstractions也是作者提出的一种观点http://www.joelonsoftware.com/articles/LeakyAbstractions.html)
 
posted @ 2014-10-03 13:56  hilbertdu  阅读(315)  评论(0编辑  收藏  举报