一直以来想写点关于代码质量的心得,碍于自身的懒惰。今天终究找到一个提前忙完工作的午后,可以先让自己的思路开动起来了。
最终促使我开始整理自己对于代码质量的看法,还多亏了前阵子认识的Long小朋友,他及时地向我推荐了《The Art of Readable Code》这本书(下文简称ARC)。在看过了马叔叔的《Clean Code》和《Clean Coder》之后,这本书彻底让我沉迷于代码质量之中了。
我就将每天读书所做的笔记和自己的想法综合起来,再加上原来历次项目之中的研究心得,陆续写出来,与大家分享。也欢迎更多的代码洁癖者一起交流。
可读性一直是代码质量管控所追求的目标之一。没有这个,后面的可维护和可修改都不太容易达成。可读性怎么强调都不为过,ARC一书的作者对具备可读性的代码给出说法是:
花最少的时间就能理解的代码。
私以为这个定义是我暂时能找到的比较合理的解释了。可读性的研究应该从横向与纵向两个层面展开。
横向地说,团队内部,尤其是开源项目,更是要维护个成员之间对代码的理解度。封装与理解并不矛盾,封装是为了更好的让客户代码理解其结构与功能,更为恰当地使用这个模块。侥幸将自己搞不清爽的代码以封装的名义塞到某个莫名其妙的类中,终归会导致那部分代码的缺陷通过接口或频繁使用暴露出来。这一点是个大问题,以后专文再述。
在同一个项目或者同一个模块工作的开发者之间一定要彼此理解对方所写的代码。这就是我为何一再强调“交叉代码评审”的原因。为了减少横向沟通的时间,提高合作效率,大家最好是在一份理解的比较透彻的代码库上进行协作。如果发现某段代码出现难于理解的情况,立即自我检查、并于其他同事讨论,大家一起拿出来个所有人都易于理解的办法来。切勿打出“时间紧,以后再说”(一旦说出这种话,我还没见过以后还有人会主动回过头来整理代码)的挡箭牌或者“勿要过分偏执于代码质量”这样的理由。
实际上,很多工作中的沟通不畅都是源于对产品代码、设计、架构的理解不到位。对于这个问题,我提倡采用极限编程或与其等效的协同式结对或组团工作法,同时缩短代码评审周期。所有一线程序员一定要经常举行20-25分钟左右(番茄法)的技术讨论对话。
纵向地说,代码的理解实际上也是对程序员本人业务能力的一种拓展训练。在没有系统地学习敏捷开发等代码管控技术之前,我经常对自己几个月前、几周前甚至三天前所写的代码一头雾水,根本不清楚当时是在何种情境下写出那些代码的。协同工作时更为严重,我们不仅要理解自己很久以前写过的代码,还要理解其他同事甚至离职人员的遗留代码。如果不及时进行品质管控,整个项目就无法继续健康的运作下去,因为对当前模块的编写势必要引入原来的既有模块,而且为了应对复杂多变的需求,必须经常把原来的代码拿出来晒太阳,以便理顺思路,尽速应对需求。长时间进行有意识的质量训练,就可以在工作中积累大量的代码范式和可复用模块,并且在遇到新工作的新需求时及时从脑中呼出原有的高品质解决方案。
在谈到对“理解”的判定标准时,ARC的作者提出的标准也比较有参考性。他们认为,代码阅读者能够对其作出修改、能够指出其中的Bug、能够理解它与其余部分代码是如何进行沟通的。做到了上述这些,才算“完全理解了代码”。小翔我雅以为是。在进行上述我提到的横向和纵向沟通时,都必须以“彻底”理解为沟通目标,不要蒙混过去。
除了横向和纵向的沟通问题之外,还有一个问题就是如何处理代码质量与其他工程要素之间的关系,例如代码执行效率、软件设计与架构、代码是否易于测试等等。很多反对花时间提升代码质量的人都拿这些来做文章。不过依我在实际工作中的感觉是,如果因为代码品质得不到保证而导致沟通不畅,那么相应的效率、架构、易测试性都可能随之出现问题,因为它们最终都要落实到具体代码与具体开发者身上,一个尊崇易读性的编码环境才能催生执行高效、架构合理、易于测试的代码。
原来我之所以没有及时将代码质量的相关心得与想法总结起来,很重要的一个原因是代码质量所涉及的知识点太多、太散,而且和其他话题联系颇多。在开始读ARC这本书之后,我决定依照可读性为主线,把我这个有代码洁癖者的所思所想整理成系列文章,这样更方便按照主题去阅读、研究。
说到到可读性的具体判定标准,这则是要靠每个人在学习、工作中不断总结出来的。很多教材整本书所讲的就是如何依据一系列的经验法则来指导编程,比如《Clean Code》。我以为不妨按照代码层级,将可读性的研究分为“零散代码改观”、“简化逻辑与循环”、“宏观结构重整”三个部分。零散代码改观涉及函数或方法内部的命名与注释等仅涵盖数行代码的初阶问题。而逻辑与循环则是函数或方法代码中的核心部分,它们通常以代码块或数行与流程相关的代码组成。针对此部分的品质提升,主要表现在梳理控制流、简化表达式、考究循环控制变量等问题。结构重整就是在更为宏观的函数、类、包等级别上进行质量管控。
为了说明代码质量不是随心所欲能决定的,我就翻炒一下ARC中的几个小例子(小栗子)。
跟着感觉走,有时不可靠。
Node node = list.head; if (node == null) return; while (node.next != null) { print(node.data); node = node.next; } if (node != null) print(node.data);
这段代码当然不如下面这段简洁,这大家凭感觉就能看出来:
for (Node node = list.head; node != null; node = node.next) print(node.data);
然而
return exponent >=0? mantissa *(1<< exponent): mantissa /(1<<-exponent);
与
if (exponent >= 0) { return mantissa * (1 << exponent); } else { return mantissa / (1 << -exponent); }
谁好谁坏就难说了。第一个更简洁,第二个更具亲和力。
短代码未必不好
assert((!(bucket = findBucket(key)))||!bucket.isOccupied());
上一段代码的可读性不如下一段:
bucket = findBucket(key); if(bucket !=null)assert(!bucket.isOccupied());
多写点注释也好
// 更快地执行" hash = (65599 * hash) + c"hash =(hash <<6)+(hash <<16)- hash + c;
上面这段代码多亏了这个注释,否则立刻滑入杂技代码的深渊。
下几篇系列文章将讲述如何选取易读的标识符名称。
代码质量随想录(二):必也正名乎
代码质量随想录(三):名字好,误会少
代码质量随想录(四):排版,不只是为了漂亮