• 1摘要
  • 2什么是好代码
    • 2.1好代码的定义
    • 2.2可读的代码
      • 2.2.1逐字翻译
      • 2.2.2遵循约定
      • 2.2.3文档和注释
      • 2.2.4推荐阅读
    • 2.3可发布的代码
      • 2.3.1处理异常
      • 2.3.2处理并发
      • 2.3.3优化性能
      • 2.3.4日志
      • 2.3.5扩展阅读
    • 2.4可维护的代码
      • 2.4.1避免重复
      • 2.4.2模块划分
      • 2.4.3简洁与抽象
      • 2.4.4推荐阅读
  • 3结语

1.摘要

这是烂代码系列的第二篇,在文章中我会跟你们讨论一下如何尽量高效和客观的评价代码的优劣。 
在发布了关于烂代码的那些事(上)以后,发现这篇文章居然意外的很受欢迎,不少人也描(tu)述(cao)了各自代码中这样或者那样的问题。 
最近部门在组织bootcamp,正好我负责培训代码质量部分,在培训课程中让你们花了很多时间去讨论、改进、完善本身的代码。虽然刚毕业的同窗对于代码质量都很用心,但最终呈现出来的质量仍然没能达到“十分优秀”的程度。 究其缘由,主要是不了解好的代码“应该”是什么样的。
html

2.什么是好代码

写代码的第一步是理解什么是好代码。在准备bootcamp的课程的时候,我就为这个问题犯了难,我尝试着用一些精确的定义区分出“优等品”、“良品”、“不良品”;可是在总结的过程当中,关于“什么是好代码”的描述却大多没有可操做性java

2.1.好代码的定义

随便从网上搜索了一下“优雅的代码”,找到了下面这样的定义:linux

Bjarne Stroustrup,C++之父:git

  • 逻辑应该是清晰的,bug难以隐藏;
  • 依赖最少,易于维护;
  • 错误处理彻底根据一个明确的策略;
  • 性能接近最佳化,避免代码混乱和无原则的优化;
  • 整洁的代码只作一件事。

Grady Booch,《面向对象分析与设计》做者:程序员

  • 整洁的代码是简单、直接的;
  • 整洁的代码,读起来像是一篇写得很好的散文;
  • 整洁的代码永远不会掩盖设计者的意图,而是具备少许的抽象和清晰的控制行。

Michael Feathers,《修改代码的艺术》做者:github

  • 整洁的代码看起来老是像很在意代码质量的人写的;
  • 没有明显的须要改善的地方;
  • 代码的做者彷佛考虑到了全部的事情。

看起来彷佛说的都颇有道理,但是实际评判的时候却难以参考,尤为是对于新人来讲,如何理解“简单的、直接的代码”或者“没有明显的须要改善的地方”?web

而实践过程当中,不少同窗也确实面对这种问题:对本身的代码老是处在一种内心不踏实的状态,或者是本身以为很好了,可是却被其余人认为很烂,甚至有几回我和新同窗由于代码质量的标准一连讨论好几天,却谁也说服不了谁:咱们都坚持本身对于好代码的标准才是正确的。算法

在经历了无数次code review以后,我以为这张图彷佛总结的更好一些:数据库

code review

代码质量的评价标准某种意义上有点相似于文学做品,好比对小说的质量的评价主要来自于它的读者,由个体主观评价造成一个相对客观的评价。并非依靠字数,或者做者使用了哪些修辞手法之类的看似彻底客观但实际没有什么意义的评价手段。编程

但代码和小说还有些不同,它实际存在两个读者:计算机和程序员。就像上篇文章里说的,即便全部程序员都看不懂这段代码,它也是能够被计算机理解并运行的。

因此对于代码质量的定义我须要于从两个维度分析:主观的,被人类理解的部分;还有客观的,在计算机里运行的情况。

既然存在主观部分,那么就会存在个体差别,对于同一段代码评价会由于看代码的人的水平不一样而得出不同的结论,这也是大多数新人面对的问题:他们没有一个能够执行的评价标准,因此写出来的代码质量也很难提升。

有些介绍代码质量的文章讲述的都是倾向或者原则,虽说的很对,可是实际指导做用不大。因此在这篇文章里我但愿尽量把评价代码的标准用(我自认为)与实际水平无关的评价方式表示出来。

2.2.可读的代码

在权衡好久以后,我决定把可读性的优先级排在前面:一个程序员更但愿接手一个有bug可是看的懂的工程,仍是一个没bug可是看不懂的工程?若是是后者,能够直接关掉这个网页,去作些对你来讲更有意义的事情。

2.2.1.逐字翻译

在不少跟代码质量有关的书里都强调了一个观点:程序首先是给人看的,其次才是能被机器执行,我也比较认同这个观点。在评价一段代码能不能让人看懂的时候,我习惯让做者把这段代码逐字翻译成中文,试着组成句子,以后把中文句子读给另外一我的没有看过这段代码的人听,若是另外一我的能听懂,那么这段代码的可读性基本就合格了。

用这种判断方式的缘由很简单:其余人在理解一段代码的时候就是这么作的。阅读代码的人会一个词一个词的阅读,推断这句话的意思,若是仅靠句子没法理解,那么就须要联系上下文理解这句代码,若是简单的联系上下文也理解不了,可能还要掌握更多其它部分的细节来帮助推断。大部分状况下,理解一句代码在作什么须要联系的上下文越多,意味着代码的质量越差。

逐字翻译的好处是能让做者能轻易的发现那些只有本身知道的、没有体如今代码里的假设和可读性陷阱。没法从字面意义上翻译出本来意思的代码大多都是烂代码,好比“ms表明messageService“,或者“ms.proc()是发消息“,或者“tmp表明当前的文件”。

2.2.2.遵循约定

约定包括代码和文档如何组织,注释如何编写,编码风格的约定等等,这对于代码将来的维护很重要。对于遵循何种约定没有一个强制的标准,不过我更倾向于遵照更多人的约定。

与开源项目保持风格一致通常来讲比较靠谱,其次也能够遵照公司内部的编码风格。可是若是公司内部的编码风格和当前开源项目的风格冲突比较严重,每每表明着这个公司的技术倾向于封闭,或者已经有些跟不上节奏了。

可是不管如何,遵照一个约定总比本身创造出一些规则要好不少,这下降了理解、沟通和维护的成本。若是一个项目本身创造出了一些奇怪的规则,可能意味着做者看过的代码不够多。

一个工程是否遵循了约定每每须要代码阅读者有必定经验,或者须要借助checkstyle这样的静态检查工具。若是感受无处下手,那么大部分状况下跟着google作应该不会有什么大问题:能够参考google code style,其中一部分有对应的中文版

另外,没有必要纠结于遵循了约定到底有什么收益,就好像走路是靠左好仍是靠右好同样,即便得出告终论也没有什么意义,大部分约定只要遵照就能够了。

2.2.3.文档和注释

文档和注释是程序很重要的部分,他们是理解一个工程或项目的途径之一。二者在某些场景下定位会有些重合或者交叉(好比javadoc实际能够算是文档)。

对于文档的标准很简单,能找到、能读懂就能够了,通常来讲我比较关心这几类文档:

  1. 对于项目的介绍,包括项目功能、做者、目录结构等,读者应该能3分钟内大体理解这个工程是作什么的。
  2. 针对新人的QuickStart,读者按照文档说明应该能在1小时内完成代码构建和简单使用。
  3. 针对使用者的详细说明文档,好比接口定义、参数含义、设计等,读者能经过文档了解这些功能(或接口)的使用方法。

有一部分注释实际是文档,好比以前提到的javadoc。这样能把源码和注释放在一块儿,对于读者更清晰,也能简化很多文档的维护的工做。

还有一类注释并不做为文档的一部分,好比函数内部的注释,这类注释的职责是说明一些代码自己没法表达的做者在编码时的思考,好比“为何这里没有作XXX”,或者“这里要注意XXX问题”。

通常来讲我首先会关心注释的数量:函数内部注释的数量应该不会有不少,也不会彻底没有,我的的经验值是滚动几屏幕看到一两处左右比较正常。过多的话可能意味着代码自己的可读性有问题,而若是一点都没有可能意味着有些隐藏的逻辑没有说明,须要考虑适当的增长一点注释了。

其次也须要考虑注释的质量:在代码可读性合格的基础上,注释应该提供比代码更多的信息。文档和注释并非越多越好,它们可能会致使维护成本增长。关于这部分的讨论能够参考简洁部分的内容。

2.2.4.推荐阅读

  • 《代码整洁之道》

2.3.可发布的代码

新人的代码有一个比较典型的特征,因为缺乏维护项目的经验,写的代码总会有不少考虑不到的地方。好比说测试的时候彷佛没什么异常,项目发布以后才发现有不少意料以外的情况;而出了问题以后不知道从哪下手排查,或者仅能让系统处于一个并不稳定的状态,依靠一些巧合勉强运行。

2.3.1.处理异常

新手程序员广泛没有处理异常的意识,但代码的实际运行环境中充满了异常:服务器会死机,网络会超时,用户会胡乱操做,不怀好意的人会恶意攻击你的系统。

我对一段代码异常处理能力的第一印象来自于单元测试的覆盖率。大部分异常难以在开发或者测试环境里复现,即便有专业的测试团队也很难在集成测试环境中模拟全部的异常状况。

而单元测试能够比较简单的模拟各类异常状况,若是一个模块的单元测试覆盖率连50%都不到,很难想象这些代码考虑了异常状况下的处理,即便考虑了,这些异常处理的分支都没有被验证过,怎么期望实际运行环境中出现问题时表现良好呢?

2.3.2.处理并发

我收到的不少简历里都写着:精通并发编程/熟悉多线程机制,诸如此类,跟他们聊的时候也说的头头是道,什么锁啊互斥啊线程池啊同步啊信号量啊一堆一堆的名词口若悬河。而给应聘者一个实际场景,让应聘者写一段很简单的并发编程的小程序,能写好的却很少。

实际上并发编程也确实很难,若是说写好同步代码的难度为5,那么并发编程的难度能够达到100。这并非危言耸听,不少看似稳定的程序,在面对并发场景的时候仍然可能出现问题:好比最近咱们就碰到了一个linux kernel在调用某个系统函数时因为同步问题而出现crash的状况。

而是否高质量的实现并发编程的关键并非是否应用了某种同步策略,而是看代码中是否保护了共享资源:

  • 局部变量以外的内存访问都有并发风险(好比访问对象的属性,访问静态变量等)
  • 访问共享资源也会有并发风险(好比缓存、数据库等)。
  • 被调用方若是不是声明为线程安全的,那么颇有可能存在并发问题(好比java的hashmap)。
  • 全部依赖时序的操做,即便每一步操做都是线程安全的,仍是存在并发问题(好比先删除一条记录,而后把记录数减一)。

前三种状况可以比较简单的经过代码自己分辨出来,只要简单培养一下本身对于共享资源调用的敏感度就能够了。

可是对于最后一种状况,每每很难简单的经过看代码的方式看出来,甚至出现并发问题的两处调用并非在同一个程序里(好比两个系统同时读写一个数据库,或者并发的调用了一个程序的不一样模块等)。可是,只要是代码里出现了不加锁的,访问共享资源的“先作A,再作B”之类的逻辑,可能就须要提升警戒了。

2.3.3.优化性能

性能是评价程序员能力的一个重要指标,不少程序员也对程序的性能津津乐道。但程序的性能很难直接经过代码看出来,每每要借助于一些性能测试工具,或者在实际环境中执行才能有结果。

若是仅从代码的角度考虑,有两个评价执行效率的办法:

  • 算法的时间复杂度,时间复杂度高的程序运行效率必然会低。
  • 单步操做耗时,单步耗时高的操做尽可能少作,好比访问数据库,访问io等。

而实际工做中,也会见到一些程序员过于热衷优化效率,相对的会带来程序易读性的下降、复杂度提升、或者增长工期等等。对于这类状况,简单的办法是让做者说出这段程序的瓶颈在哪里,为何会有这个瓶颈,以及优化带来的收益。

固然,不管是优化不足仍是优化过分,判断性能指标最好的办法是用数听说话,而不是单纯看代码,性能测试这部份内容有些超出这篇文章的范围,就不详细展开了。

2.3.4.日志

日志表明了程序在出现问题时排查的难易程度,经(jing)验(chang)丰(cai)富(keng)的程序员大概都会遇到过这个场景:排查问题时就少一句日志,查不到某个变量的值不知道是什么,致使死活分析不出来问题到底出在哪。

对于日志的评价标准有三个:

  • 日志是否足够,全部异常、外部调用都须要有日志,而一条调用链路上的入口、出口和路径关键点上也须要有日志。
  • 日志的表达是否清晰,包括是否能读懂,风格是否统一等。这个的评价标准跟代码的可读性同样,不重复了。
  • 日志是否包含了足够的信息,这里包括了调用的上下文、外部的返回值,用于查询的关键字等,便于分析信息。

对于线上系统来讲,通常能够经过调整日志级别来控制日志的数量,因此打印日志的代码只要不对阅读形成障碍,基本上都是能够接受的。

2.3.5.扩展阅读

  • 《Release It!: Design and Deploy Production-Ready Software》(不要看中文版,翻译的实在是太烂了)
  • Numbers Everyone Should Know

2.4.可维护的代码

相对于前两类代码来讲,可维护的代码评价标准更模糊一些,由于它要对应的是将来的状况,通常新人很难想象如今的一些作法会对将来形成什么影响。不过根据个人经验,通常来讲,只要反复的提问两个问题就能够了:

  • 他离职了怎么办?
  • 他没这么作怎么办?

2.4.1.避免重复

几乎全部程序员都知道要避免拷代码,可是拷代码这个现象仍是不可避免的成为了程序可维护性的杀手。

代码重复分为两种:模块内重复和模块间重复。不管何种重复,都在必定程度上说明了程序员的水平有问题,模块内重复的问题更大一些,若是在同一个文件里都能出现大片重复的代码,那表示他什么难以想象的代码都有可能写出来。

对于重复的判断并不须要反复阅读代码,通常来讲现代的IDE都提供了检查重复代码的工具,只需点几下鼠标就能够了。

除了代码重复以外,不少热衷于维护代码质量的程序员新人很容易出现另外一类重复:信息重复。

我见过一些新人喜欢在每行代码前面写一句注释,好比:

// 成员列表的长度>0而且<200
if(memberList.size() > 0 && memberList.size() < 200) {
    // 返回当前成员列表
    return memberList;
}
看起来似

看起来彷佛很好懂,可是几年以后,这段代码就变成了:

// 成员列表的长度>0而且<200
if(memberList.size() > 0 && memberList.size() < 200 || (tmp.isOpen() && flag)) {
    // 返回当前成员列表
    return memberList;
}

再以后可能会改为这样:

// edit by axb 2015.07.30
// 成员列表的长度>0而且<200
//if(memberList.size() > 0 && memberList.size() < 200 || (tmp.isOpen() && flag)) {
//     返回当前成员列表
//    return memberList;
//}
if(tmp.isOpen() && flag) {
    return memberList;
}

随着项目的演进,无用的信息会越积越多,最终甚至让人没法分辨哪些信息是有效的,哪些是无效的。

若是在项目中发现好几个东西都在作同一件事情,好比经过注释描述代码在作什么,或者依靠注释替代版本管理的功能,那么这些代码也不能称为好代码。

2.4.2.模块划分

模块内高内聚与模块间低耦合是大部分设计遵循的标准,经过合理的模块划分可以把复杂的功能拆分为更易于维护的更小的功能点。

通常来讲能够从代码长度上初步评价一个模块划分的是否合理,一个类的长度大于2000行,或者一个函数的长度大于两屏幕都是比较危险的信号。

另外一个可以体现模块划分水平的地方是依赖。若是一个模块依赖特别多,甚至出现了循环依赖,那么也能够反映出做者对模块的规划比较差,从此在维护这个工程的时候颇有可能出现牵一发而动全身的状况。

通常来讲有很多工具能提供依赖分析,好比IDEA中提供的Dependencies Analysis功能,学会这些工具的使用对于评价代码质量会有很大的帮助。

值得一提的是,绝大部分状况下,不恰当的模块划分也会伴随着极低的单元测试覆盖率:复杂模块的单元测试很是难写的,甚至是不可能完成的任务。因此直接查看单元测试覆盖率也是一个比较靠谱的评价方式。

2.4.3.简洁与抽象

只要提到代码质量,必然会提到简洁、优雅之类的形容词。简洁这个词实际涵盖了不少东西,代码避免重复是简洁、设计足够抽象是简洁,一切对于提升可维护性的尝试实际都是在试图作减法。

编程经验不足的程序员每每不能意识到简洁的重要性,乐于捣鼓一些复杂的玩意并乐此不疲。但复杂是代码可维护性的天敌,也是程序员能力的一道门槛。

跨过门槛的程序员应该有能力控制逐渐增加的复杂度,总结和抽象出事物的本质,并体现到本身设计和编码中。一个程序的生命周期也是在由简入繁到化繁为简中不断迭代的过程。

对于这部分我难以总结出简单易行的评价标准,它更像是一种思惟方式,除了要理解、还须要练习。多看、多想、多交流,不少时候能够简化的东西会大大超出原先的预计。

2.2.4.推荐阅读

  • 《重构-改善既有代码的设计》
  • 《设计模式-可复用面向对象软件的基础》
  • 《Software Architecture Patterns-Understanding Common Architecture Patterns and When to Use Them》

3.结语

这篇文章主要介绍了一些评价代码质量优劣的手段,这些手段中,有些比较客观,有些主观性更强。以前也说过,对代码质量的评价是一件主观的事情,这篇文章里虽然列举了不少评价手段。可是实际上,不少我认为没有问题的代码也会被其余人吐槽,因此这篇文章只能算是初稿,更多内容还须要从此继续补充和完善。

虽然每一个人对于代码质量评价的倾向都不同,可是整体来讲评价代码质量的能力能够被比做程序员的“品味”,评价的准确度会随着自身经验的增长而增加。在这个过程当中,须要随时保持思考、学习和批判的精神。

下篇文章里,会谈一谈具体如何提升本身的代码质量。

[转自]http://blog.2baxb.me/archives/1378