关于烂代码的那些事

开发 后端 前端
最近写了不少代码,review了不少代码,也做了不少重构,总之是对着烂代码工作了几周。为了抒发一下这几周里好几次到达崩溃边缘的情绪,我决定写一篇文章谈一谈烂代码的那些事。 这里是上篇,谈一谈烂代码产生的原因和现象。

1.摘要

最近写了不少代码,review了不少代码,也做了不少重构,总之是对着烂代码工作了几周。为了抒发一下这几周里好几次到达崩溃边缘的情绪,我决定写一篇文章谈一谈烂代码的那些事。 这里是上篇,谈一谈烂代码产生的原因和现象。

2.写烂代码很容易

刚入程序员这行的时候经常听到一个观点:你要把精力放在ABCD(需求文档/功能设计/架构设计/理解原理)上,写代码只是把想法翻译成编程语言而已,是一个没什么技术含量的事情。

当时的我在听到这种观点时会有一种近似于高冷的不屑:你们就是一群傻X,根本不懂代码质量的重要性,这么下去迟早有一天会踩坑,呸。

可是几个月之后,他们似乎也没怎么踩坑。而随着编程技术一直在不断发展,带来了更多的我以前认为是傻X的人加入到程序员这个行业中来。

语言越来越高级、封装越来越完善,各种技术都在帮助程序员提高生产代码的效率,依靠层层封装,程序员真的不需要了解一丁点技术细节,只要把需求里的内容逐行翻译出来就可以了。

很多程序员不知道要怎么组织代码、怎么提升运行效率、底层是基于什么原理,他们写出来的是在我心目中烂成一坨翔一样的代码。

但是那一坨翔一样代码竟然他妈的能正常工作。

即使我认为他们写的代码是坨翔,但是从不接触代码的人的视角来看(比如说你的boss),代码编译过了,测试过了,上线运行了一个月都没出问题,你还想要奢求什么?

所以,即使不情愿,也必须承认,时至今日,写代码这件事本身没有那么难了。

3.烂代码终究是烂代码

但是偶尔有那么几次,写烂代码的人离职了之后,事情似乎又变得不一样了。

想要修改功能时却发现程序里充斥着各种无法理解的逻辑、改完之后莫名其妙的bug一个接一个,接手这个项目的人开始漫无目的的加班,并且原本一个挺乐观开朗的人渐渐的开始喜欢问候别人祖宗了。

我总结了几类经常被艹祖宗的烂代码:

3.1.意义不明

能力差的程序员容易写出意义不明的代码,他们不知道自己究竟在做什么.

就像这样:

  1. public void save() { 

  2. for(int i=0;i<100;i++) { 

  3. //防止保存失败,重试100次 

  4. document.save(); 

对于这类程序员,我一般建议他们转行。

3.2.不说人话

不说人话是新手最经常出现的问题,直接的表现就是写了一段很简单的代码,其他人却看不懂。

比如下面这段:

  1. public boolean getUrl(Long id) { 

  2. UserProfile up = us.getUser(ms.get(id).getMessage().aid); 

  3. if (up == null) { 

  4. return false

  5. if (up.type == 4 || ((up.id >> 2) & 1) == 1) { 

  6. return false

  7. if(Util.getUrl(up.description)) { 

  8. return true

  9. else { 

  10. return false

很多程序员喜欢简单的东西:简单的函数名、简单的变量名、代码里翻来覆去只用那么几个单词命名;能缩写就缩写、能省略就省略、能合并就合并。这类人写出来的代码里充斥着各种g/s/gos/of/mss之类的全世界没人懂的缩写,或者一长串不知道在做什么的连续调用。

还有很多程序员喜欢复杂,各种宏定义、位运算之类写的天花乱坠,生怕代码让别人一下子看懂了会显得自己水平不够。

简单的说,他们的代码是写给机器的,不是给人看的。

3.3.不恰当的组织

不恰当的组织是高级一些的烂代码,程序员在写过一些代码之后,有了基本的代码风格,但是对于规模大一些的工程的掌控能力不够,不知道代码应该如何解耦、分层和组织。

这种反模式的现象是经常会看到一段代码在工程里拷来拷去;某个文件里放了一大坨堆砌起来的代码;一个函数堆了几百上千行;或者一个简单的功能七拐八绕的调了几十个函数,在某个难以发现的猥琐的小角落里默默的调用了某些关键逻辑。

这类代码大多复杂度高,难以修改,经常一改就崩;而另一方面,创造了这些代码的人倾向于修改代码,畏惧创造代码,他们宁愿让原本复杂的代码一步步变得更复杂,也不愿意重新组织代码。当你面对一个几千行的类,问为什么不把某某逻辑提取出来的时候,他们会说:

“但是,那样就多了一个类了呀。”

3.4.假设和缺少抽象

相对于前面的例子,假设这种反模式出现的场景更频繁,花样更多,始作俑者也更难以自己意识到问题。比如:

  1. public String loadString() { 

  2. File file = new File("c:/config.txt"); 

  3. // read something 

文件路径变更的时候,会把代码改成这样:

  1. public String loadString(String name) { 

  2. File file = new File(name); 

  3. // read something 

需要加载的内容更丰富的时候,会再变成这样:

  1. public String loadString(String name) { 

  2. File file = new File(name); 

  3. // read something 

  4. public Integer loadInt(String name) { 

  5. File file = new File(name); 

  6. // read something 

之后可能会再变成这样:

  1. public String loadString(String name) { 

  2. File file = new File(name); 

  3. // read something 

  4. public String loadStringUtf8(String name) { 

  5. File file = new File(name); 

  6. // read something 

  7. public Integer loadInt(String name) { 

  8. File file = new File(name); 

  9. // read something 

  10. public String loadStringFromNet(String url) { 

  11. HttpClient ... 

  12. public Integer loadIntFromNet(String url) { 

  13. HttpClient ... 

这类程序员往往是项目组里开发效率比较高的人,但是大量的业务开发工作导致他们不会做多余的思考,他们的口头禅是:“我每天要做XX个需求”或者“先做完需求再考虑其他的吧”。

这种反模式表现出来的后果往往是代码很难复用,面对deadline的时候,程序员迫切的想要把需求落实成代码,而这往往也会是个循环:写代码的时候来不及考虑复用,代码难复用导致之后的需求还要继续写大量的代码。

一点点积累起来的大量的代码又带来了组织和风格一致性等问题,最后形成了一个新功能基本靠拷的遗留系统。

3.5.还有吗

烂代码还有很多种类型,沿着功能-性能-可读-可测试-可扩展这条路线走下去,还能看到很多匪夷所思的例子。

那么什么是烂代码?个人认为,烂代码包含了几个层次:

如果只是一个人维护的代码,满足功能和性能要求倒也足够了。

如果在一个团队里工作,那就必须易于理解和测试,让其它人员有能力修改各自的代码。

同时,越是处于系统底层的代码,扩展性也越重要。

所以,当一个团队里的底层代码难以阅读、耦合了上层的逻辑导致难以测试、或者对使用场景做了过多的假设导致难以复用时,虽然完成了功能,它依然是坨翔一样的代码。

3.6.够用的代码

而相对的,如果一个工程的代码难以阅读,能不能说这个是烂代码?很难下定义,可能算不上好,但是能说它烂吗?如果这个工程自始至终只有一个人维护,那个人也维护的很好,那它似乎就成了“够用的代码”。

很多工程刚开始可能只是一个人负责的小项目,大家关心的重点只是代码能不能顺利的实现功能、按时完工。

过上一段时间,其他人参与时才发现代码写的有问题,看不懂,不敢动。需求方又开始催着上线了,怎么办?只好小心翼翼的只改逻辑而不动结构,然后在注释里写上这么实现很ugly,以后明白内部逻辑了再重构。

再过上一段时间,有个相似的需求,想要复用里面的逻辑,这时才意识到代码里做了各种特定场景的专用逻辑,复用非常麻烦。为了赶进度只好拷代码然后改一改。问题解决了,问题也加倍了。

几乎所有的烂代码都是从“够用的代码”演化来的,代码没变,使用代码的场景发生变了,原本够用的代码不符合新的场景,那么它就成了烂代码。

4.重构不是万能药

程序员最喜欢跟程序员说的谎话之一就是:现在进度比较紧,等X个月之后项目进度宽松一些再去做重构。

不能否认在某些(极其有限的)场景下重构是解决问题的手段之一,但是写了不少代码之后发现,重构往往是程序开发过程中最复杂的工作。花一个月写的烂代码,要花更长的时间、更高的风险去重构。

曾经经历过几次忍无可忍的大规模重构,每一次重构之前都是找齐了组里的高手,开了无数次分析会,把组内需求全部暂停之后才敢开工,而重构过程中往往哀嚎遍野,几乎每天都会出上很多意料之外的问题,上线时也几乎必然会出几个问题。

从技术上来说,重构复杂代码时,要做三件事:理解旧代码、分解旧代码、构建新代码。而待重构的旧代码往往难以理解;模块之间过度耦合导致牵一发而动全身,不易控制影响范围;旧代码不易测试导致无法保证新代码的正确性。

这里还有一个核心问题,重构的复杂度跟代码的复杂度不是线性相关的。比如有1000行烂代码,重构要花1个小时,那么5000行烂代码的重构可能要花2、3天。要对一个失去控制的工程做重构,往往还不如重写更有效率。

而抛开具体的重构方式,从受益上来说,重构也是一件很麻烦的事情:它很难带来直接受益,也很难量化。这里有个很有意思的现象,基本关于重构的书籍无一例外的都会有独立的章节介绍“如何向boss说明重构的必要性”。

重构之后能提升多少效率?能降低多少风险?很难答上来,烂代码本身就不是一个可以简单的标准化的东西。

举个例子,一个工程的代码可读性很差,那么它会影响多少开发效率?

你可以说:之前改一个模块要3天,重构之后1天就可以了。但是怎么应对“不就是做个数据库操作吗为什么要3天”这类问题?烂代码“烂”的因素有不确 定性、开发效率也因人而异,想要证明这个东西“确实”会增加两天开发时间,往往反而会变成“我看了3天才看懂这个函数是做什么的”或者“我做这么简单的修 改要花3天”这种神经病才会去证明的命题。

而另一面,许多技术负责人也意识到了代码质量和重构的必要性,“那就重构嘛”,或者“如果看到问题了,那就重构”。上一个问题解决了,但实际上关于 重构的代价和收益仍然是一笔糊涂账,在没有分配给你更多资源、没有明确的目标、没有具体方法的情况下,很难想象除了有代码洁癖的人还有谁会去执行这种莫名 其妙的任务。

于是往往就会形成这种局面:

  • 不写代码的人认为应该重构,重构很简单,无论新人还是老人都有责任做重构。

  • 写代码老手认为应该迟早应该重构,重构很难,现在凑合用,这事别落在我头上。

  • 写代码的新手认为不出bug就谢天谢地了,我也不知道怎么重构。

5.写好代码很难

与写出烂代码不同的是,想写出好代码有很多前提:

  • 理解要开发的功能需求。

  • 了解程序的运行原理。

  • 做出合理的抽象。

  • 组织复杂的逻辑。

  • 对自己开发效率的正确估算。

  • 持续不断的练习。

写出好代码的方法论很多,但我认为写出好代码的核心反而是听起来非常low的“持续不断的练习”。这里就不展开了,留到下篇再说。

很多程序员在写了几年代码之后并没有什么长进,代码仍然烂的让人不忍直视,原因有两个主要方面:

  • 环境是很重要的因素之一,在烂代码的熏陶下很难理解什么是好代码,知道的人大部分也会选择随波逐流。

  • 还有个人性格之类的说不清道不明的主观因素,写出烂代码的程序员反而都是一些很好相处的人,他们往往热爱公司团结同事平易近人工作任劳任怨–只是代码很烂而已。

而工作几年之后的人很难再说服他们去提高代码质量,你只会反复不断的听到:“那又有什么用呢?”或者“以前就是这么做的啊?”之类的说法。

那么从源头入手,提高招人时对代码的质量的要求怎么样?

前一阵面试的时候增加了白板编程、最近又增加了上机编程的题目。发现了一个现象:一个人工作了几年、做过很多项目、带过团队、发了一些文章,不一定能代表他代码写的好;反之,一个人代码写的好,其它方面的能力一般不会太差。

举个例子,最近喜欢用“写一个代码行数统计工具”作为面试的上机编程题目。很多人看到题目之后第一反映是,这道题太简单了,这不就是写写代码嘛。

从实际效果来看,这道题识别度却还不错。

首先,题目足够简单,即使没有看过《面试宝典》之类书的人也不会吃亏。而题目的扩展性很好,即使提前知道题目,配合不同的条件,可以变成不同的题目。比如要求按文件类型统计行数、或者要求提高统计效率、或者统计的同时输出某些单词出现的次数,等等。

从考察点来看,首先是基本的树的遍历算法;其次有一定代码量,可以看出程序员对代码的组织能力、对问题的抽象能力;上机编码可以很简单的看出应聘者是不是很久没写程序了;还包括对于程序易用性和性能的理解。

最重要的是,最后的结果是一个完整的程序,我可以按照日常工作的标准去评价程序员的能力,而不是从十几行的函数里意淫这个人在日常工作中大概会有什么表现。

但即使这样,也很难拍着胸脯说,这个人写的代码质量没问题。毕竟面试只是代表他有写出好代码的能力,而不是他将来会写出好代码。

6.悲观的结语

说了那么多,结论其实只有两条,作为程序员:

  • 不要奢望其他人会写出高质量的代码

  • 不要以为自己写出来的是高质量的代码

如果你看到了这里还没有丧失希望,那么可以期待一下这篇文章的第二部分,关于如何提高代码质量的一些建议和方法。

#p#

1.摘要

这是烂代码系列的第二篇,在文章中我会跟大家讨论一下如何尽可能高效和客观的评价代码的优劣。

在发布了《关于烂代码的那些事(上)》之后,发现这篇文章竟然意外的很受欢迎,很多人也描(tu)述(cao)了各自代码中这样或者那样的问题。

最近部门在组织bootcamp,正好我负责培训代码质量部分,在培训课程中让大家花了不少时间去讨论、改进、完善自己的代码。虽然刚毕业的同学对 于代码质量都很用心,但最终呈现出来的质量仍然没能达到“十分优秀”的程度。 究其原因,主要是不了解好的代码“应该”是什么样的。

2.什么是好代码

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

2.1.好代码的定义

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

Bjarne Stroustrup,C++之父:

  • 逻辑应该是清晰的,bug难以隐藏;

  • 依赖最少,易于维护;

  • 错误处理完全根据一个明确的策略;

  • 性能接近最佳化,避免代码混乱和无原则的优化;

  • 整洁的代码只做一件事。

Grady Booch,《面向对象分析与设计》作者:

  • 整洁的代码是简单、直接的;

  • 整洁的代码,读起来像是一篇写得很好的散文;

  • 整洁的代码永远不会掩盖设计者的意图,而是具有少量的抽象和清晰的控制行。

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

  • 整洁的代码看起来总是像很在乎代码质量的人写的;

  • 没有明显的需要改善的地方;

  • 代码的作者似乎考虑到了所有的事情。

看起来似乎说的都很有道理,可是实际评判的时候却难以参考,尤其是对于新人来说,如何理解“简单的、直接的代码”或者“没有明显的需要改善的地方”?

而实践过程中,很多同学也确实面对这种问题:对自己的代码总是处在一种心里不踏实的状态,或者是自己觉得很好了,但是却被其他人认为很烂,甚至有几次我和新同学因为代码质量的标准一连讨论好几天,却谁也说服不了谁:我们都坚持自己对于好代码的标准才是正确的。

在经历了无数次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都提供了检查重复代码的工具,只需点几下鼠标就可以了。

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

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

  1. // 成员列表的长度>0并且<200 
  2. if(memberList.size() > 0 && memberList.size() < 200) { 
  3. // 返回当前成员列表 
  4. return memberList; 

看起来似乎很好懂,但是几年之后,这段代码就变成了:

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

再之后可能会改成这样:

  1. // edit by axb 2015.07.30 
  2. // 成员列表的长度>0并且<200 
  3. //if(memberList.size() > 0 && memberList.size() < 200 || (tmp.isOpen() && flag)) { 
  4. // 返回当前成员列表 
  5. // return memberList; 
  6. //} 
  7. if(tmp.isOpen() && flag) { 
  8. 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.结语

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

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

下篇文章里,会谈一谈具体如何提高自己的代码质量。

 

责任编辑:王雪燕 来源: 2baxb
相关推荐

2015-09-14 09:28:47

2017-04-10 18:10:31

2015-07-23 09:30:43

烂代码程序员

2012-05-01 08:06:49

手机

2012-07-13 00:03:08

WEB前端开发WEB开发

2019-12-10 08:00:46

Kata容器Linux

2021-03-18 16:05:20

SSD存储故障

2009-02-19 10:21:00

路由多WAN口

2012-01-02 19:30:22

iPad

2011-08-22 16:42:43

SqliteiPad

2011-08-01 17:31:25

Xcode开发 Cocoa

2011-07-04 13:51:02

QT 对象 模型

2022-09-09 08:08:28

开源项目服务

2011-07-19 15:33:57

iPhone

2011-06-30 10:59:43

2011-12-02 10:32:23

Java

2015-08-19 09:10:37

程序员面试

2012-04-05 10:49:40

服务器SSL证书

2021-05-17 08:18:35

Java内存模型JMM

2024-03-18 00:00:05

Java服务JVM
点赞
收藏

51CTO技术栈公众号