从去年到今年,陆陆续续看完了《代码大全》、《重构》、《代码整洁之道》、《程序员修炼之道》以及《The Art of Readable Code》,获益匪浅。下面就分享几条我赞同并信奉的编程哲学,顺便废话几句。
本文适合有一定编程经验的读者阅读,高手请轻喷:)
代码是写给人看的(Coding for Reading)
请先思考,评价一段代码优劣最重要的标准是什么?
有个著名的图,相信大家都见过,讲的是Code Review时被骂WTF的次数越少,代码的质量就越高。这虽然有点无厘头,但是却不无道理。
《The Art of Readable Code》中告诉我们,评价一段代码的质量的最佳标准是可读性,即别人理解代码意图所需要的时间。
可读性有什么用,我写出一段代码,能完成目标,能通过测试不就行了吗?
不管你是团队开发,还是一个人单干,只要项目还在运作,代码总是要有人来维护的。如果有一天你不在或者离开了,别人应该能很轻松的看懂你的代码,而不是猜来猜去,最后还要来问你,甚至弃用整段代码重新再写。作为一个负责任的开发者,应该尊重自己的劳动成果,也尊重别人的时间。
当你写下一段代码时,脑子里有清晰的逻辑(比如这里必须这么做,因为……这里不能那么做,因为……)。而别人看这段代码时,脑中是一片空白(这个变量是干什么的?为什么要多加一层判断?这个数为什么是 2 不是 3 ?),除非能完整复现你当时脑中的逻辑,否则就不能理解这段代码。
所以我们应该做的是,把写代码时脑中想到的逻辑的每一个细节,尽可能地写到代码以及注释中,从而帮助后来的阅读者快速复原整个逻辑。简单来说可以有以下的方法:
用常量代替魔术数
在一段代码中出现 if ( level > 3 ) ...的时候,也许你会想:为什么是 3 不是 4 呢?这就是所谓的魔术数。通过上下文也许你可以判断出这个 3 是指最高等级,但是可能这段代码中 3 出现了好几次。你会迷惑:它们是不是都是一个意思?当我要把最高等级改成 4 的时候,是不是应该修改所有的 3 ?
如果代码的作者定义一个常量MAX_LEVEL = 3,同时还有一个常量USERS_PER_PAGE = 3,并在不同的地方使用不同的常量,就不会有这样的混淆了。
使用富含信息的类名、变量名和函数名
doit(); $return = $str2 . ':' . $str3; return $return;
这样的代码,简直就是人工混淆过的。你会发现这些代码根本不能帮助你理解代码的含义。
也许你该写成这样?
handleError(); $error_message = $error_code . ':' . $error_status; return $error_message;
在注释中说明一段代码存在的原因,而不是行为
$('#item').html(''); //清空item的内容
这样的注释有意义么?用自然语言重新描述一次代码的行为,除了徒增维护时的工作量外没有任何价值。你应该说明为什么这样做,以供别人看到这段代码时明白你是怎么想的,并决定如何修改或者对待这段代码。
$('#item').html(''); //先清空容器的内容,否则可能导致内容重复
写得越少越好(Less is More)
这个规则的使用性太强了,我简单说说减少逻辑层次和缩小函数体这两个方面吧。
减少逻辑层次
当逻辑层次超过三层时,理解这段代码的难度会急剧上升。我相信谁也不喜欢去读一个n层括号的表达式,或者面对n层缩进的条件判断/循环。
对于复杂的表达式,通过提取中间变量来降低表达式的逻辑层次,保证每个表达式的逻辑层次不超过二层。
对于多层条件判断,大多数情况可以用防御式编程将其简化成单层的条件判断,尽早return或者exit。此外,单行的if-else判断往往可以用三元操作符替换。如果判断实在太多,也许你该重新设计一下结构了。
缩小函数体
记得有一位语言的创始人说过:“我不喜欢比我的头还大的函数”。
事实上,大家都喜欢短小精干、一眼就能看到底的代码。简洁明快的代码有助于别人迅速理解代码的意图,也方便快速定位问题。如果一个函数要滚动屏幕才能看全,那你往往要不断地来回滚动,并强迫自己记住一些信息,再返回去看另一部分,这样做会非常累。
随着函数体不断膨胀,理解它所需要的时间随之增加,出错的几率也会大大提升。而且越大的函数,可维护性和复用性越差。当部分代码逻辑需要修改时,不能快速定位到要修改的位置,也难以确定函数体其他位置是否也需要对应的修改。
当函数尺寸失控时,首先要想到的是,有没有其他方法,用更少的代码完成这个任务?能不能用正则表达式?能不能用查表法?有没有内置的库函数可以利用?
面对一个无法再简化的流程,将其拆分成细粒度的步骤,将每个步骤的相关代码分离出来,提取成子函数,再给子函数起一个漂亮的名字。这样可以降低理解主流程的难度,在做修改时也可以通过函数名快速定位,而且因为相关的代码都在一起,不容易漏改。
也许你会怀疑调用函数所造成的性能损失,我想说现在这个时代,手机都马上四核了……
不要重复(Don't Repeat Yourself)
看过《重构》一书后,我看到代码中任何重复的地方都如见眼中钉。
重复是万恶之源,当你发现你在对代码的不同部分进行同样的修改时就要警惕了。改的地方越多,就越可能出错。也许忘了改一个地方,也许错改或者删除了周边的代码……永远不要让这种事情发生!
将重复的代码提炼成子函数
$('#count').text( + $('#count').text() + 1 ); ... $('#count').text( + $('#count').text() + 1 ); ...
如果一段相同的代码出现两次,基本上你还会第三次用到它,所以很有必要将其提炼成子函数。这样不仅可以减少代码量,还可以降低维护的难度。
countPlusOne(); ... countPlusOne(); ... function countPlusOne() { $('#count').text( + $('#count').text() + 1 ); //以后修改只用改这里就好了 }
如果有几段代码很相似,往往可以提取其共性逻辑,使用不同的调用参数进行区分。
countChange( +1 ); ... countChange( -1 ); ... function countChange( change ) { $('#count').text( + $('#count').text() + change ); }
用循环减少重复
$item[ 'id' ] = $_POST[ 'id' ]; $item[ 'name' ] = $_POST[ 'name' ]; $item[ 'mail' ] = $_POST[ 'mail' ]; $item[ 'qq' ] = $_POST[ 'qq' ];
如果有大块逻辑雷同只有一两个地方更改的代码,往往可以用循环来解决:
foreach ( array( 'id', 'name', 'mail', 'qq' ) as $key ) { $item[ $key ] = $_POST[ $key ]; }