http://kb.cnblogs.com/page/526768/html
============上篇============java
最近写了很多代码,review了很多代码,也作了很多重构,总之是对着烂代码工做了几周。为了抒发一下这几周里好几回到达崩溃边缘的情绪,我决定写一篇文章谈一谈烂代码的那些事。这里是上篇,谈一谈烂代码产生的缘由和现象。linux
刚入程序员这行的时候常常听到一个观点:你要把精力放在ABCD(需求文档/功能设计/架构设计/理解原理)上,写代码只是把想法翻译成编程语言而已,是一个没什么技术含量的事情。git
当时的我在听到这种观点时会有一种近似于高冷的不屑:大家就是一群傻X,根本不懂代码质量的重要性,这么下去早晚有一天会踩坑,呸。程序员
但是几个月以后,他们彷佛也没怎么踩坑。而随着编程技术一直在不断发展,带来了更多的我之前认为是傻X的人加入到程序员这个行业中来。github
语言愈来愈高级、封装愈来愈完善,各类技术都在帮助程序员提升生产代码的效率,依靠层层封装,程序员真的不须要了解一丁点技术细节,只要把需求里的内容逐行翻译出来就能够了?面试
不少程序员不知道要怎么组织代码、怎么提高运行效率、底层是基于什么原理,他们写出来的是在我心目中烂成一坨翔同样的代码。算法
可是那一坨翔同样代码居然他妈的能正常工做。sql
即便我认为他们写的代码是坨翔,可是从不接触代码的人的视角来看(好比说你的boss),代码编译过了,测试过了,上线运行了一个月都没出问题,你还想要奢求什么?数据库
因此,即便不情愿,也必须认可,时至今日,写代码这件事自己没有那么难了。
可是偶尔有那么几回,写烂代码的人离职以后,事情彷佛又变得不同了。
想要修改功能时却发现程序里充斥着各类没法理解的逻辑,改完以后莫名其妙的bug一个接一个,接手这个项目的人开始漫无目的的加班,而且本来一个挺乐观开朗的人渐渐的开始喜欢问候别人祖宗了。
我总结了几类常常被艹祖宗的烂代码:
能力差的程序员容易写出意义不明的代码,他们不知道本身究竟在作什么.
就像这样:
public void save() { for(int i=0;i<100;i++) { //防止保存失败,重试100次 document.save(); } }
对于这类程序员,我通常建议他们转行。
不说人话是新手最常常出现的问题,直接的表现就是写了一段很简单的代码,其余人却看不懂。
好比下面这段:
public boolean getUrl(Long id) { UserProfile up = us.getUser(ms.get(id).getMessage().aid); if (up == null) { return false; } if (up.type == 4 || ((up.id >> 2) & 1) == 1) { return false; } if(Util.getUrl(up.description)) { return true; } else { return false; } }
不少程序员喜欢简单的东西:简单的函数名、简单的变量名、代码里翻来覆去只用那么几个单词命名;能缩写就缩写、能省略就省略、能合并就合并。这类人写出来的代码里充斥着各类g/s/gos/of/mss之类的全世界没人懂的缩写,或者一长串不知道在作什么的连续调用。
还有不少程序员喜欢复杂,各类宏定义、位运算之类写的天花乱坠,生怕代码让别人一会儿看懂了会显得本身水平不够。
简单的说,他们的代码是写给机器的,不是给人看的。
不恰当的组织是高级一些的烂代码,程序员在写过一些代码以后,有了基本的代码风格,可是对于规模大一些的工程的掌控能力不够,不知道代码应该如何解耦、分层和组织。
这种反模式的现象是常常会看到一段代码在工程里拷来拷去;某个文件里放了一大坨堆砌起来的代码;一个函数堆了几百上千行;或者一个简单的功能七拐八绕的调了几十个函数,在某个难以发现的猥琐的小角落里默默的调用了某些关键逻辑。
这类代码大多复杂度高,难以修改,常常一改就崩;而另外一方面,创造了这些代码的人倾向于修改代码,畏惧创造代码,他们宁愿让本来复杂的代码一步步变得更复杂,也不肯意从新组织代码。当你面对一个几千行的类,问为何不把某某逻辑提取出来的时候,他们会说:
“可是,那样就多了一个类了呀。”
相对于前面的例子,假设这种反模式出现的场景更频繁,花样更多,始做俑者也更难以本身意识到问题。好比:
public String loadString() { File file = new File("c:/config.txt"); // read something }
文件路径变动的时候,会把代码改为这样:
public String loadString(String name) { File file = new File(name); // read something }
须要加载的内容更丰富的时候,会再变成这样:
public String loadString(String name) { File file = new File(name); // read something } public Integer loadInt(String name) { File file = new File(name); // read something }
以后可能会再变成这样:
public String loadString(String name) { File file = new File(name); // read something } public String loadStringUtf8(String name) { File file = new File(name); // read something } public Integer loadInt(String name) { File file = new File(name); // read something } public String loadStringFromNet(String url) { HttpClient ... } public Integer loadIntFromNet(String url) { HttpClient ... }
这类程序员每每是项目组里开发效率比较高的人,可是大量的业务开发工做致使他们不会作多余的思考,他们的口头禅是:“我天天要作XX个需求”或者“先作完需求再考虑其余的吧”。
这种反模式表现出来的后果每每是代码很难复用,面对deadline的时候,程序员迫切的想要把需求落实成代码,而这每每也会是个循环:写代码的时候来不及考虑复用,代码难复用致使以后的需求还要继续写大量的代码。
一点点积累起来的大量的代码又带来了组织和风格一致性等问题,最后造成了一个新功能基本靠拷的遗留系统。
烂代码还有不少种类型,沿着功能-性能-可读-可测试-可扩展这条路线走下去,还能看到不少匪夷所思的例子。
那么什么是烂代码?我的认为,烂代码包含了几个层次:
因此,当一个团队里的底层代码难以阅读、耦合了上层的逻辑致使难以测试、或者对使用场景作了过多的假设致使难以复用时,虽然完成了功能,它依然是坨翔同样的代码。
而相对的,若是一个工程的代码难以阅读,能不能说这个是烂代码?很难下定义,可能算不上好,可是能说它烂吗?若是这个工程自始至终只有一我的维护,那我的也维护的很好,那它彷佛就成了“够用的代码”。
不少工程刚开始可能只是一我的负责的小项目,你们关心的重点只是代码能不能顺利的实现功能、按时完工。
过上一段时间,其余人参与时才发现代码写的有问题,看不懂,不敢动。需求方又开始催着上线了,怎么办?只好当心翼翼的只改逻辑而不动结构,而后在注释里写上这么实现很ugly,之后明白内部逻辑了再重构。
再过上一段时间,有个类似的需求,想要复用里面的逻辑,这时才意识到代码里作了各类特定场景的专用逻辑,复用很是麻烦。为了赶进度只好拷代码而后改一改。问题解决了,问题也加倍了。
几乎全部的烂代码都是从“够用的代码”演化来的,代码没变,使用代码的场景发生变了,本来够用的代码不符合新的场景,那么它就成了烂代码。
程序员最喜欢跟程序员说的谎言之一就是:如今进度比较紧,等X个月以后项目进度宽松一些再去作重构。
不可否认在某些(极其有限的)场景下重构是解决问题的手段之一,可是写了很多代码以后发现,重构每每是程序开发过程当中最复杂的工做。花一个月写的烂代码,要花更长的时间、更高的风险去重构。
曾经经历过几回忍无可忍的大规模重构,每一次重构以前都是找齐了组里的高手,开了无数次分析会,把组内需求所有暂停以后才敢开工,而重构过程当中每每哀嚎遍野,几乎天天都会出上不少意料以外的问题,上线时也几乎必然会出几个问题。
从技术上来讲,重构复杂代码时,要作三件事:理解旧代码、分解旧代码、构建新代码。而待重构的旧代码每每难以理解;模块之间过分耦合致使牵一发而动全身,不易控制影响范围;旧代码不易测试致使没法保证新代码的正确性。
这里还有一个核心问题,重构的复杂度跟代码的复杂度不是线性相关的。好比有1000行烂代码,重构要花1个小时,那么5000行烂代码的重构可能要花二、3天。要对一个失去控制的工程作重构,每每还不如重写更有效率。
而抛开具体的重构方式,从受益上来讲,重构也是一件很麻烦的事情:它很难带来直接受益,也很难量化。这里有个颇有意思的现象,基本关于重构的书籍无一例外的都会有独立的章节介绍“如何向boss说明重构的必要性”。
重构以后能提高多少效率?能下降多少风险?很难答上来,烂代码自己就不是一个能够简单的标准化的东西。
举个例子,一个工程的代码可读性不好,那么它会影响多少开发效率?
你能够说:以前改一个模块要3天,重构以后1天就能够了。可是怎么应对“不就是作个数据库操做吗,为何要3天”这类问题?烂代码“烂”的因素有不肯定性,开发效率也因人而异,想要证实这个东西“确实”会增长2天开发时间,每每反而会变成“我看了3天才看懂这个函数是作什么的”或者“我作这么简单的修改要花3天”这种神经病才会去证实的命题。
而另外一面,许多技术负责人也意识到了代码质量和重构的必要性,“那就重构嘛”,或者“若是看到问题了,那就重构”。上一个问题解决了,但实际上关于重构的代价和收益仍然是一笔糊涂帐,在没有分配给你更多资源、没有明确的目标、没有具体方法的状况下,很难想象除了有代码洁癖的人还有谁会去执行这种莫名其妙的任务。
因而每每就会造成这种局面:
与写出烂代码不一样的是,想写出好代码有不少前提:
写出好代码的方法论不少,但我认为写出好代码的核心反而是听起来很是low的“持续不断的练习”。这里就不展开了,留到下篇再说。
不少程序员在写了几年代码以后并无什么长进,代码仍然烂的让人不忍直视,缘由有两个主要方面:
而工做几年以后的人很难再说服他们去提升代码质量,你只会反复不断的听到:“那又有什么用呢?”或者“之前就是这么作的啊?”之类的说法。
那么从源头入手,提升招人时对代码的质量的要求怎么样?
前一阵面试的时候增长了白板编程,最近又增长了上机编程的题目。发现了一个现象:一我的工做了几年、作过不少项目、带过团队、发了一些文章,不必定能表明他代码写的好;反之,一我的代码写的好,其它方面的能力通常不会太差。
举个例子,最近喜欢用“写一个代码行数统计工具”做为面试的上机编程题目。不少人看到题目以后第一反映是,这道题太简单了,这不就是写写代码嘛。
从实际效果来看,这道题识别度却还不错。
首先,题目足够简单,即便没有看过《面试宝典》之类书的人也不会吃亏。而题目的扩展性很好,即便提早知道题目,配合不一样的条件,能够变成不一样的题目。好比要求按文件类型统计行数、或者要求提升统计效率、或者统计的同时输出某些单词出现的次数,等等。
从考察点来看,首先是基本的树的遍历算法;其次有必定代码量,能够看出程序员对代码的组织能力、对问题的抽象能力;上机编码能够很简单的看出应聘者是否是好久没写程序了;还包括对于程序易用性和性能的理解。
最重要的是,最后的结果是一个完整的程序,我能够按照平常工做的标准去评价程序员的能力,而不是从十几行的函数里意淫这我的在平常工做中大概会有什么表现。
但即便这样,也很难拍着胸脯说,这我的写的代码质量没问题。毕竟面试只是表明他有写出好代码的能力,而不是他未来会写出好代码。
说了那么多,结论其实只有两条,做为程序员:
若是你看到了这里尚未丧失但愿,那么能够期待一下这篇文章的第二部分,关于如何提升代码质量的一些建议和方法。
============中篇============
这是烂代码系列的第二篇,在文章中我会跟你们讨论一下如何尽量高效和客观的评价代码的优劣。
在发布了关于烂代码的那些事(上)以后,发现这篇文章居然意外的很受欢迎,不少人也描(tu)述(cao)了各自代码中这样或者那样的问题。
最近部门在组织bootcamp,正好我负责培训代码质量部分,在培训课程中让你们花了很多时间去讨论、改进、完善本身的代码。虽然刚毕业的同窗对于代码质量都很用心,但最终呈现出来的质量仍然没能达到“十分优秀”的程度。 究其缘由,主要是不了解好的代码“应该”是什么样的。
写代码的第一步是理解什么是好代码。在准备bootcamp课程的时候,我就为这个问题犯了难,我尝试着用一些精确的定义区分出“优等品”、“良品”、“不良品”,可是在总结的过程当中,关于“什么是好代码”的描述却大多没有可操做性
随便从网上搜索了一下“优雅的代码”,找到了下面这样的定义:
Bjarne Stroustrup,C++之父:
- 逻辑应该是清晰的,bug难以隐藏;
- 依赖最少,易于维护;
- 错误处理彻底根据一个明确的策略;
- 性能接近最佳化,避免代码混乱和无原则的优化;
- 整洁的代码只作一件事。
Grady Booch,《面向对象分析与设计》做者:
- 整洁的代码是简单、直接的;
- 整洁的代码,读起来像是一篇写得很好的散文;
- 整洁的代码永远不会掩盖设计者的意图,而是具备少许的抽象和清晰的控制行。
Michael Feathers,《修改代码的艺术》做者:
- 整洁的代码看起来老是像很在意代码质量的人写的;
- 没有明显的须要改善的地方;
- 代码的做者彷佛考虑到了全部的事情。
看起来彷佛说的都颇有道理,但是实际评判的时候却难以参考,尤为是对于新人来讲,如何理解“简单的、直接的代码”或者“没有明显的须要改善的地方”?
而实践过程当中,不少同窗也确实面对这种问题:对本身的代码老是处在一种内心不踏实的状态,或者是本身以为很好了,可是却被其余人认为很烂,甚至有几回我和新同窗由于代码质量的标准一连讨论好几天,却谁也说服不了谁:咱们都坚持本身对于好代码的标准才是正确的。
在经历了无数次code review以后,我以为这张图彷佛总结的更好一些:
代码质量的评价标准某种意义上有点相似于文学做品,好比对小说的质量的评价主要来自于它的读者,由个体主观评价造成一个相对客观的评价。并非依靠字数,或者做者使用了哪些修辞手法之类的看似彻底客观但实际没有什么意义的评价手段。
但代码和小说还有些不同,它实际存在两个读者:计算机和程序员。就像上篇文章里说的,即便全部程序员都看不懂这段代码,它也是能够被计算机理解并运行的。
因此对于代码质量的定义我须要于从两个维度分析:主观的,被人类理解的部分;还有客观的,在计算机里运行的情况。
既然存在主观部分,那么就会存在个体差别,对于同一段代码评价会由于看代码的人的水平不一样而得出不同的结论,这也是大多数新人面对的问题:他们没有一个能够执行的评价标准,因此写出来的代码质量也很难提升。
有些介绍代码质量的文章讲述的都是倾向或者原则,虽说的很对,可是实际指导做用不大。因此在这篇文章里我但愿尽量把评价代码的标准用(我自认为)与实际水平无关的评价方式表示出来。
在权衡好久以后,我决定把可读性的优先级排在前面:一个程序员更但愿接手一个有bug可是看得懂的工程,仍是一个没bug可是看不懂的工程?若是是后者,能够直接关掉这个网页,去作些对你来讲更有意义的事情。
在不少跟代码质量有关的书里都强调了一个观点:程序首先是给人看的,其次才是能被机器执行,我也比较认同这个观点。在评价一段代码能不能让人看懂的时候,我习惯让做者把这段代码逐字翻译成中文,试着组成句子,以后把中文句子读给另外一我的没有看过这段代码的人听,若是另外一我的能听懂,那么这段代码的可读性基本就合格了。
用这种判断方式的缘由很简单:其余人在理解一段代码的时候就是这么作的。阅读代码的人会一个词一个词的阅读,推断这句话的意思,若是仅靠句子没法理解,那么就须要联系上下文理解这句代码,若是简单的联系上下文也理解不了,可能还要掌握更多其它部分的细节来帮助推断。大部分状况下,理解一句代码在作什么须要联系的上下文越多,意味着代码的质量越差。
逐字翻译的好处是能让做者能轻易的发现那些只有本身知道的、没有体如今代码里的假设和可读性陷阱。没法从字面意义上翻译出本来意思的代码大多都是烂代码,好比“ms表明messageService“,或者“ms.proc()是发消息“,或者“tmp表明当前的文件”。
约定包括代码和文档如何组织,注释如何编写,编码风格的约定等等,这对于代码将来的维护很重要。对于遵循何种约定没有一个强制的标准,不过我更倾向于遵照更多人的约定。
与开源项目保持风格一致通常来讲比较靠谱,其次也能够遵照公司内部的编码风格。可是若是公司内部的编码风格和当前开源项目的风格冲突比较严重,每每表明着这个公司的技术倾向于封闭,或者已经有些跟不上节奏了。
可是不管如何,遵照一个约定总比本身创造出一些规则要好不少,这下降了理解、沟通和维护的成本。若是一个项目本身创造出了一些奇怪的规则,可能意味着做者看过的代码不够多。
一个工程是否遵循了约定,每每须要代码阅读者有必定经验,或者须要借助checkstyle这样的静态检查工具。若是感受无处下手,那么大部分状况下跟着google作应该不会有什么大问题:能够参考google code style,其中一部分有对应的中文版。
另外,没有必要纠结于遵循了约定到底有什么收益,就好像走路是靠左好仍是靠右好同样,即便得出告终论也没有什么意义,大部分约定只要遵照就能够了。
文档和注释是程序很重要的部分,他们是理解一个工程或项目的途径之一。二者在某些场景下定位会有些重合或者交叉(好比javadoc实际能够算是文档)。
对于文档的标准很简单,能找到、能读懂就能够了,通常来讲我比较关心这几类文档:
有一部分注释实际是文档,好比以前提到的javadoc。这样能把源码和注释放在一块儿,对于读者更清晰,也能简化很多文档的维护的工做。
还有一类注释并不做为文档的一部分,好比函数内部的注释,这类注释的职责是说明一些代码自己没法表达的做者在编码时的思考,好比“为何这里没有作XXX”,或者“这里要注意XXX问题”。
通常来讲我首先会关心注释的数量:函数内部注释的数量应该不会有不少,也不会彻底没有,我的的经验值是滚动几屏幕看到一两处左右比较正常。过多的话可能意味着代码自己的可读性有问题,而若是一点都没有可能意味着有些隐藏的逻辑没有说明,须要考虑适当的增长一点注释了。
其次也须要考虑注释的质量:在代码可读性合格的基础上,注释应该提供比代码更多的信息。文档和注释并非越多越好,它们可能会致使维护成本增长。关于这部分的讨论能够参考简洁部分的内容。
《代码整洁之道》
新人的代码有一个比较典型的特征,因为缺乏维护项目的经验,写的代码总会有不少考虑不到的地方。好比说测试的时候彷佛没什么异常,项目发布以后才发现有不少意料以外的情况;而出了问题以后不知道从哪下手排查,或者仅能让系统处于一个并不稳定的状态,依靠一些巧合勉强运行。
新手程序员广泛没有处理异常的意识,但代码的实际运行环境中充满了异常:服务器会死机,网络会超时,用户会胡乱操做,不怀好意的人会恶意攻击你的系统。
我对一段代码异常处理能力的第一印象来自于单元测试的覆盖率。大部分异常难以在开发或者测试环境里复现,即便有专业的测试团队也很难在集成测试环境中模拟全部的异常状况。
而单元测试能够比较简单的模拟各类异常状况,若是一个模块的单元测试覆盖率连50%都不到,很难想象这些代码考虑了异常状况下的处理,即便考虑了,这些异常处理的分支都没有被验证过,怎么期望实际运行环境中出现问题时表现良好呢?
我收到的不少简历里都写着:精通并发编程/熟悉多线程机制,诸如此类,跟他们聊的时候也说的头头是道,什么锁啊互斥啊线程池啊同步啊信号量啊一堆一堆的名词口若悬河。而给应聘者一个实际场景,让应聘者写一段很简单的并发编程的小程序,能写好的却很少。
实际上并发编程也确实很难,若是说写好同步代码的难度为5,那么并发编程的难度能够达到100。这并非危言耸听,不少看似稳定的程序,在面对并发场景的时候仍然可能出现问题:好比最近咱们就碰到了一个linux kernel在调用某个系统函数时因为同步问题而出现crash的状况。
而是否高质量的实现并发编程的关键并非是否应用了某种同步策略,而是看代码中是否保护了共享资源:
前三种状况可以比较简单的经过代码自己分辨出来,只要简单培养一下本身对于共享资源调用的敏感度就能够了。
可是对于最后一种状况,每每很难简单的经过看代码的方式看出来,甚至出现并发问题的两处调用并非在同一个程序里(好比两个系统同时读写一个数据库,或者并发的调用了一个程序的不一样模块等)。可是,只要是代码里出现了不加锁的,访问共享资源的“先作A,再作B”之类的逻辑,可能就须要提升警戒了。
性能是评价程序员能力的一个重要指标,不少程序员也对程序的性能津津乐道。但程序的性能很难直接经过代码看出来,每每要借助于一些性能测试工具,或者在实际环境中执行才能有结果。
若是仅从代码的角度考虑,有两个评价执行效率的办法:
而实际工做中,也会见到一些程序员过于热衷优化效率,相对的会带来程序易读性的下降、复杂度提升、或者增长工期等等。对于这类状况,简单的办法是让做者说出这段程序的瓶颈在哪里,为何会有这个瓶颈,以及优化带来的收益。
固然,不管是优化不足仍是优化过分,判断性能指标最好的办法是用数听说话,而不是单纯看代码,性能测试这部份内容有些超出这篇文章的范围,就不详细展开了。
日志表明了程序在出现问题时排查的难易程度,经(jing)验(chang)丰(cai)富(keng)的程序员大概都会遇到过这个场景:排查问题时就少一句日志,查不到某个变量的值不知道是什么,致使死活分析不出来问题到底出在哪。
对于日志的评价标准有三个:
对于线上系统来讲,通常能够经过调整日志级别来控制日志的数量,因此打印日志的代码只要不对阅读形成障碍,基本上都是能够接受的。
- 《Release It!: Design and Deploy Production-Ready Software》(不要看中文版,翻译的实在是太烂了)
- Numbers Everyone Should Know
相对于前两类代码来讲,可维护的代码评价标准更模糊一些,由于它要对应的是将来的状况,通常新人很难想象如今的一些作法会对将来形成什么影响。不过根据个人经验,通常来讲,只要反复的提问两个问题就能够了:
几乎全部程序员都知道要避免拷代码,可是拷代码这个现象仍是不可避免的成为了程序可维护性的杀手。
代码重复分为两种:模块内重复和模块间重复。不管何种重复,都在必定程度上说明了程序员的水平有问题,模块内重复的问题更大一些,若是在同一个文件里都能出现大片重复的代码,那表示他什么难以想象的代码都有可能写出来。
对于重复的判断并不须要反复阅读代码,通常来讲现代的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; }
随着项目的演进,无用的信息会越积越多,最终甚至让人没法分辨哪些信息是有效的,哪些是无效的。
若是在项目中发现好几个东西都在作同一件事情,好比经过注释描述代码在作什么,或者依靠注释替代版本管理的功能,那么这些代码也不能称为好代码。
模块内高内聚与模块间低耦合是大部分设计遵循的标准,经过合理的模块划分可以把复杂的功能拆分为更易于维护的更小的功能点。
通常来讲能够从代码长度上初步评价一个模块划分的是否合理,一个类的长度大于2000行,或者一个函数的长度大于两屏幕都是比较危险的信号。
另外一个可以体现模块划分水平的地方是依赖。若是一个模块依赖特别多,甚至出现了循环依赖,那么也能够反映出做者对模块的规划比较差,从此在维护这个工程的时候颇有可能出现牵一发而动全身的状况。
通常来讲有很多工具能提供依赖分析,好比IDEA中提供的Dependencies Analysis功能,学会这些工具的使用对于评价代码质量会有很大的帮助。
值得一提的是,绝大部分状况下,不恰当的模块划分也会伴随着极低的单元测试覆盖率:复杂模块的单元测试很是难写的,甚至是不可能完成的任务。因此直接查看单元测试覆盖率也是一个比较靠谱的评价方式。
只要提到代码质量,必然会提到简洁、优雅之类的形容词。简洁这个词实际涵盖了不少东西,代码避免重复是简洁、设计足够抽象是简洁,一切对于提升可维护性的尝试实际都是在试图作减法。
编程经验不足的程序员每每不能意识到简洁的重要性,乐于捣鼓一些复杂的玩意并乐此不疲。但复杂是代码可维护性的天敌,也是程序员能力的一道门槛。
跨过门槛的程序员应该有能力控制逐渐增加的复杂度,总结和抽象出事物的本质,并体现到本身设计和编码中。一个程序的生命周期也是在由简入繁到化繁为简中不断迭代的过程。
对于这部分我难以总结出简单易行的评价标准,它更像是一种思惟方式,除了要理解,还须要练习。多看、多想、多交流,不少时候能够简化的东西会大大超出原先的预计。
- 《重构-改善既有代码的设计》
- 《设计模式-可复用面向对象软件的基础》
- 《Software Architecture Patterns-Understanding Common Architecture Patterns and When to Use Them》
这篇文章主要介绍了一些评价代码质量优劣的手段,这些手段中,有些比较客观,有些主观性更强。以前也说过,对代码质量的评价是一件主观的事情,这篇文章里虽然列举了不少评价手段。可是实际上,不少我认为没有问题的代码也会被其余人吐槽,因此这篇文章只能算是初稿,更多内容还须要从此继续补充和完善。
虽然每一个人对于代码质量评价的倾向都不同,可是整体来讲评价代码质量的能力能够被比做程序员的“品味”,评价的准确度会随着自身经验的增长而增加。在这个过程当中,须要随时保持思考、学习和批判的精神。
下篇文章里,会谈一谈具体如何提升本身的代码质量。
============下篇============
假设你已经读过烂代码系列的前两篇:了解了什么是烂代码,什么是好代码,可是仍是不可避免的接触到了烂代码(就像以前说的,几乎没有程序员能够彻底避免写出烂代码!)接下来的问题即是:如何应对这些身边的烂代码。
改善代码质量是项大工程,要开始这项工程,从可维护性入手每每是一个好的开始,但也仅仅只是开始而已。
不少人把重构当作一种一次性运动,代码实在是烂的无法改了,或者没什么新的需求了,就召集一帮人专门拿出来一段时间作重构。这在传统企业开发中多少能生效,可是对于互联网开发来讲却很难适应,缘由有两个:
这就造成了一个悖论:一方面那些变动频繁的系统更须要重构;另外一方面重构又会耽误开发进度,影响变动效率。
面对这种矛盾,一种方式是放弃重构,让代码质量天然降低,直到工程的生命周期结束,选择放弃或者重来。在某些场景下这种方式确实是有效的,可是我并不喜欢:比起让工程师不得不把天天的精力都浪费在毫无心义的事情上,为何不作些更有意义的事呢?
开始改善代码的第一步是把IDE的重构快捷键设到一个顺手的键位上,这一步很是重要:决定重构成败的每每不是你的新设计有多么牛逼,而是重构自己会占用多少时间。
好比对于IDEA来讲,我会把重构菜单设为快捷键:
这样在我想去重构的时候就能够随手打开菜单,而不是用鼠标慢慢去点,快捷键每次只能为重构节省几秒钟时间,可是却能明显减小工程师重构时的心理负担,后面会提到,小规模的重构应该跟敲代码同样属于平常开发的一部分。
我把重构分为三类:模块内部的重构、模块级别的重构、工程级别的重构。分为这三类并非由于我是什么分类强迫症,后面会看到对重构的分类对于重构的意义。
模块内部重构的目的是把模块内部的逻辑梳理清楚,而且把一个巨大无比的函数拆分红可维护的小块代码。大部分IDE都提供了对这类重构的支持,相似于:
这类重构的特色是修改基本集中在一个地方,对代码逻辑的修改不多而且基本可控,IDE的重构工具比较健壮,于是基本没有什么风险。
如下例子演示了如何经过IDE把一个冗长的函数作重构:
上图的例子中,咱们基本依靠IDE就把一个冗长的函数分红了两个子函数,接下来就能够针对子函数中的一些烂代码作进一步的小规模重构,而两个函数内部的重构也能够用一样的方法。每一次小规模重构的时间都不该该超过60s,不然将会严重影响开发的效率,进而致使重构被无尽的开发需求淹没。
在这个阶段须要对现有的模块补充一些单元测试,以保证重构的正确。不过以个人经验来看,一些简单的重构,例如修改局部变量名称,或者提取变量之类的重构,即便没有测试也是基本可靠的,若是要在快速完成模块内部重构和100%的单元测试覆盖率中选一个,我可能会选择快速完成重构。
而这类重构的收益主要是提升函数级别的可读性,以及消除超大函数,为将来进一步作模块级别的拆分打好基础。
以后的重构开始牵扯到多个模块,例如:
IDE每每对这类重构的支持有限,而且偶尔会出一些莫名其妙的问题,(例如修改类名时一不当心把配置文件里的常量字符串也给修改了)。
这类重构主要在于优化代码的设计,剥离不相关的耦合代码,在这类重构期间你须要建立大量新的类和新的单元测试,而此时的单元测试则是必须的了。
为何要建立单元测试?
- 一方面,这类重构由于涉及到具体代码逻辑的修改,靠集成测试很难覆盖全部状况,而单元测试能够验证修改的正确性。
- 更重要的意义在于,写不出单元测试的代码每每意味着糟糕的设计:模块依赖太多或者一个函数的职责过重,想象一下,想要执行一个函数却要模拟十几个输入对象,每一个对象还要模拟本身依赖的对象……若是一个模块没法被单独测试,那么从设计的角度来考虑,无疑是不合格的。
还须要啰嗦一下,这里说的单元测试只对一个模块进行测试,依赖多个模块共同完成的测试并不包含在内——例如在内存里模拟了一个数据库,并在上层代码中测试业务逻辑-这类测试并不能改善你的设计。
在这个期间还会写一些过渡用的临时逻辑,好比各类adapter、proxy或者wrapper,这些临时逻辑的生存期可能会有几个月到几年,这些看起来没什么必要的工做是为了控制重构范围,例如:
class Foo { String foo() { ... } }
若是要把函数声明改为
class Foo { boolean foo() { ... } }
那么最好经过加一个过渡模块来实现:
class FooAdaptor { private Foo foo; boolean foo() { return foo.foo().isEmpty(); } }
这样作的好处是修改函数时不须要改动全部调用方,烂代码的特征之一就是模块间的耦合比较高,每每一个函数有几十处调用,牵一发而动全身。而一旦开始全面改造,每每就会把一次看起来很简单的重构演变成几周的大工程,这种大规模重构每每是不可靠的。
每次模块级别的重构都须要精心设计,提早划分好哪些是须要修改的,哪些是须要用兼容逻辑作过渡的。但实际动手修改的时间都不该该超过一天,若是超过一天就意味着此次重构改动太多,须要控制一下修改节奏了。
不安全的重构相对而言影响范围比较大,好比:
我更建议这类操做不要用IDE,若是使用IDE,也只使用最简单的“移动”操做。这类重构单元测试已经彻底没有做用,须要集成测试的覆盖。不过也没必要紧张,若是只作“移动”的话,大部分状况下基本的冒烟测试就能够保证重构的正确性。
这类重构的目的是根据代码的层次或者类型进行拆分,切断循环依赖和结构上不合理的地方。若是不知道如何拆分,能够依照以下思路:
而这类重构绝对不能跟正常的需求开发并行执行:代码冲突几乎没法避免,而且会让全部人崩溃。个人作法通常是在这类重构前先演练一次:把模块按大体的想法拖来拖去,经过编译器找到依赖问题,在平常上线中把容易处理的依赖问题解决掉;而后集中团队里的精英,通知全部人暂停开发,花最多二、3天时间把全部问题集中突击掉,新的需求都在新代码的基础上进行开发。
若是历史包袱实在过重,能够把这类重构也拆成几回作:先大致拆分红几块,再分别拆分。不管如何,这类重构务必控制好变动范围,一次严重的合并冲突有可能让团队中的全部人几个周缓不过劲来。
典型的重构周期相似下面的过程:
性能这个话题愈来愈多的被人提起,随便收到一份简历不写上点什么熟悉高并发、作过性能优化之类的彷佛都很差意思跟人打招呼。
说个真事,几年前在我作某公司的ERP项目,里面有个功能是生成一个报表。而使用咱们系统的公司里有一我的,他天天要在下班前点一下报表,导出到excel,再发一封邮件出去。
问题是,那个报表每次都要2,3分钟才能生成。
我当时正年轻气盛,看到有个两分钟才能生成的报表一下就来了兴趣,翻出了那段不知道谁写的代码,发现里面用了3层循环,每次都会去数据库查一次数据,再把一堆数据拼起来,一股脑塞进一个tableview里。
面对这种代码,我还能作什么呢?
作了这些以后,界面只须要不到1s就能展现出来了,不过我要说的不是这个。
后来我去客户公司给那个操做员演示新的模块的时候,点一下,刷,数据出来了。那我的很惊恐的看着我,而后问我,是否是数据不许了。
再后来,我又加了一个功能,那个模块每次打开以后都会显示一个进度条,上面的标题是“正在校验数据……”,进度条走完大概要1分钟左右,我跟那人说校验数据计算量很大,会比较慢。固然,实际上那60秒里程序毛事都没作,只是在一点点的更新那个进度条(我还作了个彩蛋,在读进度的时候按上上下下左右左右BABA的话就能够加速10倍读条…)。客户很开心,说感受数据准确多了,固然,他没发现彩蛋。
我写了这么多,是想让你明白一个事实:大部分程序对性能并不敏感。而少数对性能敏感的程序里,一大半能够靠调节参数解决性能问题;最后那一小撮须要修改代码优化性能的程序里,性价比高的工做又是少数。
什么是性价比?回到刚才的例子里,我作了那么多事,每件事的收益是多少?
我如今遇到的不少面试者说程序优化时老是喜欢说一些玄乎的东西:调用栈、尾递归、内联函数、GC调优……可是当我问他们:把一个普通函数改为内联函数是把原来运行速度是多少的程序优化成多少了,却不多有人答出来;或者是扭扭捏捏的说,应该不少,由于这个函数会被调用不少遍。我再问会被调用多少遍,每遍是多长时间,就答不上来了。
因此关于性能优化,我有两个观点:
至于具体的优化措施,无外乎几类:
关于性能优化的话题还能够讲不少内容,不过对于这篇文章来讲有点跑题,这里就再也不详细展开了。
前一阵听一个技术分享,说是他们在编程的时候要考虑太阳黑子对cpu计算的影响,或者是农民伯伯的猪把基站拱塌了之类的特殊场景。若是要优化程序的健壮性,那么有时候就不得不去考虑这些极端状况对程序的影响。
大部分的人应该不用考虑太阳黑子之类的高深的问题,可是咱们须要考虑一些常见的特殊场景,大部分程序员的代码对于一些特殊场景都会有或多或少考虑不周全的地方,例如:
常规的方法确实可以发现代码中的一些bug,可是到了复杂的生产环境中时,总会出现一些彻底没有想到的问题。虽然我也想了好久,遗憾的是,对于健壮性来讲,我并无找到什么立竿见影的解决方案,所以,我只能谨慎的提出一点点建议:
看了上面的那么多东西以后,你能够想一下这么个场景:
在你作了不少事情以后,代码质量彷佛有了质的飞跃。正当你觉得终于能够摆脱每天踩屎的日子了的时候,某次不当心瞥见某个类又长到几千行了。
你愤怒的翻看提交日志,想找出罪魁祸首是谁,结果却发现天天都会有人往文件里提交那么十几二十行代码,每次的改动看起来都没什么问题,可是日积月累,一年年过去,当初花了九牛二虎之力重构的工程又成了一坨烂代码……
任何一个对代码有追求的程序员都有可能遇到这种问题,技术在更新,需求在变化,公司人员会流动,而代码质量总会在不经意间偷偷的变差……
想要改善代码质量,最后每每就会变成改善生存环境。
团队须要一套统一的编码规范、统一的语言版本、统一的编辑器配置、统一的文件编码,若是有条件最好能使用统一的操做系统,这能避免不少无心义的工做。
就好像最近渣浪给开发所有换成了统一的macbook,一晚上之间之前的不少问题都变得不是问题了:字符集、换行符、IDE之类的问题只要一个配置文件就解决了,再也不有各类稀奇古怪的代码冲突或者不兼容的问题,也不会有人忽然提交上来一些编码格式稀奇古怪的文件了。
代码仓库基本上已是每一个公司的标配,而如今的代码仓库除了储存代码,还能够承担一些团队沟通、代码review甚至工做流程方面的任务,现在这类开源的系统不少,像gitlab(github)、Phabricator这类优秀的工具都能让代码管理变得简单不少。我这里无心讨论svn、git、hg仍是什么其它的代码管理工具更好,就算最近火热的git在复杂性和集中化管理上也有一些问题,其实我是比较期待能有替代git的工具产生的,扯远了。
代码仓库的意义在于让更多的人可以得到和修改代码,从而提升代码的生命周期,而代码自己的生命周期足够持久,对代码质量作的优化才有意义。
大多数烂代码就像癌症同样,当烂代码已经产生了能够感受到的影响时,基本已是晚期,很难治好了。
所以提早发现代码变烂的趋势很重要,这类工做能够依赖相似于checkstyle,findbug之类的静态检查工具,及时发现代码质量下滑的趋势,例如:
有了代码仓库以后,就能够把这种工具与仓库的触发机制结合起来,每次提交的时候作覆盖率、静态代码检查等工做,jenkins+sonarqube或者相似的工具就能够完成基本的流程:伴随着代码提交进行各类静态检查、运行各类测试、生成报告并供人参考。
在实践中会发现,关于持续反馈的五花八门的工具不少,可是真正有用的每每只有那么一两个,大部分人并不会去在每次提交代码以后再打开一个网页点击“生成报告”,或者去登录什么系统看一下测试的覆盖率是否是变低了,所以一个一站式的系统大多数状况下会表现的更好。与其追求更多的功能,不如把有限的几个功能整合起来,例如咱们把代码管理、回归测试、代码检查、和code review集成起来,就是这个样子:
固然,关于持续集成还能够作的更多,篇幅所限,就很少说了。
不一样的团队文化会对技术产生微妙的影响,关于代码质量没有什么共同的文化,每一个公司都有本身的一套观点,而且彷佛都能说得通。
对于我本身来讲,关于代码质量是这样的观点:
如何让大多数人认同关于代码质量的观点其实是有一些难度的,大部分技术人员对代码质量的观点是既不同意、也不反对的中立态度,而代码质量就像是熵值同样,放着无论老是会像更加混乱的方向演进,而且写烂代码的成本实在是过低了,以致于一个实习生花上一个礼拜就能够毁了你花了半年精心设计的工程。
因此在提升代码质量时,务必想办法拉上团队里的其余人一块儿。虽然“引导团队提升代码质量”这件事情一开始会很辛苦,可是一旦有了一些支持者,而且有了能够参考的模板以后,剩下的工做就简单多了。
这里推荐《布道之道:引领团队拥抱技术创新》这本书,里面大部分的观点对于代码质量也是能够借鉴的。仅靠喊口号很难让其余人写出高质量的代码,让团队中的其余人体会到高质量代码的收益,比喊口号更有说服力。
优化代码质量是一件颇有意思,也颇有挑战性的事情,而挑战不光来自于代码本来有多烂,要改进的也并不仅是代码自己,还有工具、习惯、练习、开发流程、甚至团队文化这些方方面面的事情。
写这一系列文章前先后后花了半年多时间,一直处在写一点删一点的状态:我自身关于代码质量的想法和实践也在经历着不断变化。我更但愿能写出一些可以实践落地的东西,而不是喊喊口号,忽悠忽悠“敏捷开发”、“测试驱动”之类的几个名词就结束了。
可是在写文章的过程当中就会慢慢发现,不少问题的改进方法确实不是一两篇文章能够说明白的,问题之间每每又相互关联,全都展开说甚至超出了一本书的信息量,因此这篇文章也只能删去了不少内容。
我参与过不少代码质量很好的项目,也参与过一些质量很烂的项目,改进了不少项目,也放弃了一些项目,从最初的单打独斗本身改代码,到后来带领团队优化工做流程,经历了不少。不管如何,关于烂代码,我决定引用一下《布道之道》这本书里的一句话:
“‘更好’,其实不是一个目的地,而是一个方向…在当前的位置和未来的目标之间,可能有不少至关不错的地方。你只需关注离开如今的位置,而不要关心去向何方。”