好的提高代码质量的方法有哪些?
转载自:https://www.zhihu.com/question/20017545/answer/1364963057
力扣君先给大家讲一个故事吧!
01 从一个故事说起 —— 王国的工匠师
在一个古老的王国里,有两个建筑工匠——风师与土师。就实操经验而言,他们的技艺相差无几。差别在于,风师以“效率”闻名,若你要修一栋房子,找他仅需一月时间便可完成;土师以“稳定”闻名,他总是将东西归置整齐后,方才破土动工,风师只需一月完成的工程,土师需要三个月。
风师有一个大麻袋,这是他的百宝箱,他所有的工具都在里面。他把锤子命名为 1 号,斧子命名为 2 号。这样他工作时,只需叫出简短的名字:1 号拿来固定,2 号拿来伐木……用完之后,将所有工具收回麻袋。风师总是背着麻袋来,背着麻袋去。不拘泥于形式,不管哪里都潇洒走一回。风师常说:这就是“效率”的秘诀。
土师有非常多的工具箱,每种工具都放在自己的类别里。他从不给工具起别名或简称,锤子便叫锤子,斧子便叫斧子,甚至更为详细。向下又细分为羊角锤、检验锤、开山斧、板斧等等。土师开工前总是兴师动众,但他的工作从未出过大的纰漏,倒也令人钦佩不已。
风师每天总是业务繁忙,一个接一个的工程甚至排到了明年。百姓修房子,就图一个“快”字。相比之下,土师的生意就显得门可罗雀了。经常是在风师的活接不过来时,才有人请土师来帮忙建造。久而久之,人们已经默认风师的技艺高过土师,大户人家动工时,都以请到风师为荣,又因为风师总是背着他的大麻袋,坊间便尊称其为“金麻袋”。风师赚得盆满钵满,已经成为王国的小富人家了。但土师仍然是不慌不忙,细致地做着自己的工作。
王国几十年来风调雨顺,国库日渐充盈。国王大喜,这日,国王决定扩建皇宫。百官向国王举荐了风师与土师。
国王请来两位工匠,命其各修建一座凉亭,作为考验。
风师扛来他的大麻袋,准备好建材后,风风火火开始修建。10 号和砂石,15 号打桩……风师操作行云流水,但由于他的工具编号只有自己才熟悉,其他人很少能帮上忙。国王看着风师的操作,渐渐皱起了眉头。
土师将其工具箱一一打开,当他的工具都准备好时,风师已经开始打地基了。然后土师才开始建造凉亭。他指挥着建筑工用砂浆机混合砂浆,龙门架起吊……看着土师有条不紊的指挥,国王眉头方才渐渐舒展开,眼中露出赞许之色。
最终土师仍然使用了三倍于风师的时间方才完成凉亭的建造,但国王决定,任命土师为皇宫建造的御用工程师。
百官不解,只听国王说到:风师、土师的技艺巧夺天工,都是王国的能工巧匠。但风师做事随性,工具杂乱。皇宫扩建工程浩大,当人员逐渐增加,此法必乱。相比之下,风师只可偏居一隅,土师方可担此大任。
后来土师的表现恰好印证了国王的话,在土师的指挥下,皇宫的扩展只用了一年的时间。新建的皇宫恢弘大气,安全可靠。百姓在震惊之余,方才想起:由土师经手的项目,无论大小,似乎从未超过一年。风师虽以快闻名,但负责一个中等建筑时,常常会延期至两年左右。负责小型建筑尚可,当格局扩大后,土师才是最有“效率”的人。
故事的寓意很简单,工程质量与效率离不开好的建筑习惯。当我们负责一个小型项目时,我们追求速度,力求快速出成果,这时可以率性而为。当项目逐渐扩大,规范就会逐步显出它的重要性。
在软件开发中也是一样,干净的地板能减少事故发生,归置到位的工具能提升生产力。软件质量,不但依赖于架构及项目管理,而且跟代码质量息息相关。代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好的基础。
02 破窗理论与童子军军规
犯罪心理学中有一个破窗理论,以一幢有少许破窗的建筑为例,如果那些窗不被及时修理好,可能会有破坏者破坏更多的窗户。最终他们甚至会闯入建筑内,如果发现无人居住,也许就在那里定居或者纵火。一面墙,如果出现一些涂鸦没有被清洗掉,很快地,墙上就布满了乱七八糟、不堪入目的东西;一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会理所当然地将垃圾顺手丢弃在地上。
在编程中也经常出现这样的问题,当我们接手一份混乱的代码后,如果不及时加以整理,最终将会导致代码越来越混乱。这是一种基础的价值谜题,之前的混乱与期限的压力让开发者继续制造混乱。当我们 review 代码时,听到最多的辩解就是:前一个人就是这么写的。事实上,问题的症结在于:他们没有花时间让自己做得更快。
美国童子军有一条简单的军规:让营地比你来时更干净。
03 代码整洁之道
清理代码也许只是改好一个变量名,拆分一个有点过长的函数,消除一点点重复代码,清理一个嵌套 if 语句。当梳理代码时,坚守此军规:每次 review 代码,让代码比你发现它时更整洁。
一、谨慎命名
给函数和变量取个好名字是优秀程序员的基本功,取名的基本要求是 名副其实,见文知意。如果名称需要注释来补充,那就不算是个好名字。
var d // 日期
修改为:
var date
取名的第二个要求是避免误导。比如数据本身不是一个 list,那就别用 list 来命名,因为 list 一词对程序员有特殊的含义。
var personList = mapOf(“Martin” to “Author”, “LeetCode” to “Reader”)
修改为:
var personMap = mapOf(“Martin” to “Author”, “LeetCode” to “Reader”)
取名的第三个要求是去掉冗余。Variable一词永远不应当出现在变量名中,Table一词永远不应当出现在表名中。nameString 会比 name 好吗?ProductInfo 、 ProductData 和 Product 有什么区别?更糟糕的是,如果代码中同时存在 Article 和 ArticleInfo 类,程序员怎么知道该调用哪个类呢?多个意义含混的冗余词汇只会让阅读者困惑,要区分名称,就要以读者能鉴别不同之处的方式来区分。
例如:开始你用了 address 变量表示用户居住地,如上海、北京。后来又要求更详细地描述用户的居住地。如上海黄浦区、浦东区,北京海淀区、朝阳区。用什么来命名这个详细地址呢?detailAdress?smallAddress?还是 anotherAddress?这些统统都不是好的做法,牢记上述法则:以读者能鉴别不同之处的方式来区分,这时比较好的做法是修改之前的 address 变量名字为 city,再将区域的地址命名为 district。
取名的第四个要求是:严谨,不要俏皮。笔者曾经接手一位外国同事写的代码,在一个类中,这位外国同事使用了 wearClothes、wearPants 命名函数,之后又出现一个 startParty 函数。仔细理解后,笔者才发现,这是代表软件系统的第一步准备、第二步准备,然后正式启动这三个流程。或许这位同事写这个类时心情不错,将其比喻成了一个 party 的流程,但对于读者来说,梳理这三个函数的意思着实要费一番心思。事实上,此时我们最好将其命名为 firstStepOfPreparation、secondStepOfPreparation、systemBoot,宁可明确,毋为好玩。
二、函数和类
函数和类应该坚持 单一权责原则。保持高内聚,低耦合。隔离会让系统每个元素的理解变得容易。
单一权责原则:在面向对象编程领域中,单一权责原则(Single responsibility principle)规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。一个类或者模块应该有且只有一个改变的原因。
过长的函数会造成不易理解,如果某天这个函数需要修改的话,一个长长的函数会大大增加理解成本。并且,小函数也能更好地复用。
如果一个函数做了多件事,一个明显的标志是无法为它起一个精准的名字。你会觉得需要函数名需要使用 and 连接,比如 calculateAndPrintPrice,这时候最佳做法是将其拆分为 calculatePrice 和 printPrice 两个小函数。
函数的第二个规范是 尽量不要在参数中传递状态值,状态值是函数做了多件事的明显标志。例如:
fun setLoading(status: Boolean) {
if (status) {
loading.VISIBILITY = View.VISIBLE
} else {
loading.VISIBILITY = View.GONE
}
}
修改为:
fun showLoading() {
loading.VISIBILITY = View.VISIBLE
}
fun hideLoading() {
loading.VISIBILITY = View.GONE
}
函数的第三个规范是 同一个函数中的代码应该属于同一层级。良好的软件设计要求分离位于不同层级的概念,较低层级概念和较高层级概念不应混杂在一起。
fun print() {
printTitle()
// 打印详情
System.out.println(“details:”)
System.out.println(“It’s a simple sample”)
}
修改为:
fun print() {
printTitle()
printDetails()
}
fun printDetails() {
System.out.println(“details:”)
System.out.println(“It’s a simple sample”)
}
三、坏注释与好注释
注释并不是越多越好,有的注释纯属无意义的废话,例如:
// 如果count > 0,返回1,否则返回-1
if (count > 0) {
return 1
} else {
return -1
}
这些注释看起来就像是喃喃自语,或许读者阅读这些注释的时间比读代码还要长。
好的注释只应该用在必要时,用于警告其他程序员会出现某种后果的注释是有用的,例如:
fun getDataFormat(): SimpleDateFormat {
// SimpleDateFormat不是线程安全的,所以我们需要每次创建新的实例
val df = SimpleDateFormat(“yyyy-MM-dd”)
df.timeZone = TimeZone.getTimeZone(“GMT”)
return df
}
但,最好的注释是没有注释,若代码足够有表达力,用代码来展示意图往往会更好。注释总是一种失败。当我们无法找到不用注释就能表达自我的方法时,我们写了注释,这并不值得庆贺。因为注释常常会撒谎。原因很简单:程序员不能坚持维护注释,尤其是别人写的注释。当另一个人修改了代码后,往往不会去阅读上一个人写的注释,再修改注释。所以注释常常会与其所描述的代码分割开来,孑然飘零,越来越不准确。
写注释的常见动机之一是原有代码混乱。当我们阅读代码时,发现已有的代码令人困扰、乱七八糟。这时我们也许会告诉自己:“等我阅读清楚后,给它写点注释!”别那样做!最好的做法是把代码整理干净。
// 这个初始化函数用来初始化数据和初始化视图
fun init(){}
修改为:
fun initData(){}
fun initView(){}
四、良好的格式
在我们阅读报纸时,在顶部,你期望有个头条,告诉你故事的主题。然后第一段是整个故事的大纲,给出粗线条概述,但隐藏了故事细节。接着读下去,细节渐次增加,直至你了解所有的细节。
代码的格式也要像新闻文章一样,最顶部给出高层次概念,向下渐次展开细节。函数应该紧跟调用处,保证垂直方向上的靠近。如果格式混乱,读者在阅读时总会滑上滑下,导致思维跳跃,增加不必要的理解难度。
影响格式的第二个要素是 缩进与间隔,现代化的 IDE 都有格式化代码快捷键,你也可以在设置中搜索"Reformat Code",自定义格式化代码快捷键。随时格式化,并去掉多余的空行,让我们的代码保持清爽、整洁。
五、数据结构
在有的源代码中,作者采用长长的链式调用,甚至会鼓吹自己只写了一行代码便实现了此功能。事实上,绝不应该为了节省变量,写过长的链式调用,否则容易造成"火车失事"。正确的做法是遵守“得墨忒定律”:适当拆解链式调用,只和朋友谈话,不和朋友的朋友谈话,使得代码阅读和调试都更方便。
得墨忒定律(The Law of Demeter):模块不应该了解它所操作对象的内部情形。
val appId = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA).metaData.get(“app_id”)
修改为:
val applicationInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
val metaData = applicationInfo.metaData
val appId = metaData.get(“app_id”)
六、错误处理
在处理程序异常时,我们常常会用到 try / catch 代码块,而 try / catch 代码块丑陋不堪,他们搞乱了代码结构,把错误处理与正常流程混为一谈。最好的做法是 把 try 和 catch 代码块的主体部分抽离出来,另外形成函数。错误处理就是一件事。
现代化的语言都有异常机制,异常情况如果不及时加以处理,可能会导致程序崩溃。所以有的程序员对于程序的异常情况会选择返回 0 或者 -1 等错误码,以保持程序不崩溃。
别这样做,正确的做法是将错误码替换为抛出异常,只有这样才能保证出现错误时立马就可以发现,而不是让程序在错误的状态下继续执行,将来造成更加迷惑的错误。
要讨论错误处理,就一定要提及那些容易引发错误的做法。第一项就是返回 null 值。我不想去计算曾经见过多少几乎每行代码都在检查 null 值的应用程序。返回 null 值基本上就是在给自己增加工作量,也是在给调用者添乱。只要有一处没有检查 null 值,应用程序就会失控。返回 null 不如抛出 NullPointerException ,或是替换为一个 空对象。让调用者不再需要检查 null,代码也就更整洁了。
04 总结
《代码整洁之道》是软件大师 Martin 的著作,英文名为《Clean Code》。本文作于笔者第二次阅读《代码整洁之道》后,本书讲述的大多是一些编程细节和好习惯,并不是那种让人醍醐灌顶的书,它更像是一个指引,一本避坑指南,Martin 将其从业多年遇到的一些好习惯和坏习惯列举出来一一对比,告诉读者哪些习惯应该坚持,哪些习惯应该摒弃。
比如笔者在阅读之前,确实受到一些流言影响,认为注释越多越好,越细越好。Martin 告诉我,注释并不全然的好,程序员维护程序的同时往往不会花时间维护注释,导致注释说谎;以及一些喃喃自语或抖机灵的注释实际上会影响代码的阅读。有的源代码中在大括号 ‘}’ 旁添加注释,表示它是哪一个条件的闭括号,在阅读本书之前,我可能认为这是一种好习惯,甚至我可能会效仿,在阅读本书后我知道这也是一种冗余的代码,并不值得坚持。
Martin 在本书中告诉我们正确的代码、好的代码应该怎样怎样,这样即使我们在工作中,在公司凌乱不堪的源码中深陷泥淖,心中仍然知道代码的伊甸园长什么样。有了这样一个清晰的目标,我们就能一步一步地向着它前进,而不至于走偏方向。或许这也是译者将其译作“道”的原因吧。
最重要的是,Martin 向我们传递出自己“不向肮脏代码低头”,用他近乎偏执的决心清理代码、重构代码,保证它的整洁。阅读本书时,笔者心中充满了感动与敬畏,真切地感受到一位程序大师的工匠之心,我们要像照顾孩子一样照顾代码,像热爱生命一样热爱代码。
就像王国的两个工匠师一般,当我们做小项目时,我们可以化身风师,只用保证 逻辑通、能运行,但想要走得更远,我们就需要坚守土师的原则:保持好习惯、不出错。起先是我们养成习惯,后来是习惯造就我们。
以上,就是力扣君对《代码整洁之道》的阅读分享,也非常推荐你花点时间阅读这本书。
人跟人的能力千差万别,所以写出来的代码质量,肯定是不同的。有的人,写一个小逻辑,可能需要100行,而有的人,可能仅仅需要10行。代码永远会有Bug,在这方面没有最好只有更好。模块化与面向对象是实现高效无错代码的方法。高效无错代码需要思想与实践的不断反复。如何做到代码高效无错,提高代码质量的方法有哪些?又有哪些经验和技巧呢?
一、代码质量
软件是交付给用户,并由用户体验的产品;代码则是对软件正确且详细的描述,所以代码质量关系到软件产品的质量。虽然软件质量不等于代码质量,但是代码上的缺陷会严重的影响到软件产品的质量。因此,为提高代码质量的投入是值得的。
二、软件产品质量通常可以从以下六个方面去衡量
功能性,即软件是否满足了客户业务要求;
可用性,即衡量用户使用软件需要付出多大的努力;
可靠性,即软件是否能够一直处在一个稳定的状态上满足可用性;
高效性,即衡量软件正常运行需要耗费多少物理资源;
可维护性,即衡量对已经完成的软件进行调整需要多大的努力;
可移植性,即衡量软件是否能够方便地部署到不同的运行环境中;
三、提高代码质量的具体经验
- 永远不要复制代码
不惜任何代价避免重复的代码。如果一个常用的代码片段出现在了程序中的几个不同地方,重构它,把它放到一个自己的函数里。重复的代码会导致你的同事 在读你的代码时产生困惑。而重复的代码如果在一个地方修改,在另外一个地方忘记修改,就会产生到处是bug,它还会使你的代码体积变得臃肿。
- 测试你完成的代码
你知道你的代码能做什么,而且试了一下,它确实好用,但你实际上需要充分的验证它。分析所有可能的边界情况,测试在所有可能的条件下它都能如期的工作。如果有参数,传递一些预期范围外的值。传递一个null值。如果可能,让同事看看你的代码,问他们能否弄坏它。单元测试是到达这种目的的常规方法。
- 代码审查
提交你的代码之前,找个同事一起坐下来,向他解释你做了哪些修改。通常,这样做的过程中你就能发现代码中的错误,而不需要同事说一句话。这比自己审查自己的代码要有效的多得多。
- 编写不言自明的代码
勿庸置疑,注释是编程中很重要的一部分,但能够不言自明的代码跟胜一筹,因为它能让你在看代码时就能理解它。函数名变量名要慎重选择,好的变量/方法名字放到语言语义环境中时,不懂编程的人都能看懂。
- 不要使用纯数字
直接把数字嵌入代码中是一种恶习,因为无法说明它们是代表什么的。当有重复时更糟糕——相同的数字在代码的多个地方出现。如果只修改了一个,而忘记了其它的。这就导致bug。一定要用一个命名常量来代表你要表达的数字,即使它在代码里只出现一次。
- 不要做手工劳动
当做一系列动作时,人类总是喜欢犯错误。如果你在做部署工作,并且不是一步能完成的,那你就是在做错事。尽量的让工作能自动化的完成,减少人为错误。当做工作量很大的任务时,这尤其重要。
7、不要试图死磕代码加快速度,找个更加有效的算法可能更加有效。
8、代码要先做对,在弄快。先使其可靠,再让其更快。先把代码弄干净,再让它变快
9、当发现一个函数具有以下特征时,需要考虑抽取函数
(1)、过长
(2)、嵌套层数过深。
(3)、自然分块,需要使用注释描述该程序块
(4)、判断条件过于复杂
(5)、函数的某些判断分支不断变化
(6)、参数过于复杂
(7)、逻辑重复
10、局部变量应当用途单一
11、程序员应当将整洁的代码风格作为一种习惯,时刻意识到整洁代码的重要性并不断地提高重构技巧
12、关于注释
(1)、如果能用短小函数描述,则使用子函数替代注释本身。
(2)、确保注释和代码表达的意图一致,否则就失去了注释的意义。
(3)、在重要的地方写注释,不要注释满天飞,简单的重复代码的功能是毫无意义的。要让每一处注释都有价值。不要过分注释。
13、关于何时重写代码
开发团队要预留20% 的时间用作保持对原有系统的重构。剩余的时间用作开发新功能。
只要有可能,所要重构的部分进行递增修改,让用户切身感受到产品的改进,哪怕将工作时间延长。
在过去做了不少代码走读,发现了一些代码质量上比较普遍的问题,以下是其中的前五名:
臃肿的类: 类之所以会臃肿,是因为开发者缺乏对最基本的编码原则,即“单一职责原则”(SRP)的理解。这些类往往会变得很臃肿,是由于不同的且在功能上缺少关联的方法都放在了相同的类里面。
长方法: 方法之所以会变得很长主要是有以下几个原因:
许多没有关联性的、功能复杂的模块的代码都放在相同的方法内。这主要是开发者缺乏SRP的概念。
多种条件都放在同一个方法内,这在长方法内经常会发生的。这是由于缺乏McCabe代码复杂度和SRP的概念的比较。
大量的传参: 我经常遇到这几种情况,一些方法跟另一些方法进行交互,或者调用另一些方法的时候传入大量的参数。这就会出现如果更改了其中一个参数,就得在多个方法内进行更改。
常量值无处不在: 经常会发现开发者(尤其是新手)会使用一些具有明确含义的常量值(主要是魔鬼数字),但没有给它们赋予合适的常量变量。这会降低代码的可读性和可理解性。
模糊的方法名: 许多时候,以下取的方法名会影响代码的可读性和可理解性:
模糊的不具有任何意义的方法名
技术性的,却没有提及相关领域的名称
6个处理上面代码异味的重构方法(手法)
以下是6个可以用来帮助你解决80%(80-20原则)的代码质量问题的重构方法,并能帮助你成为一个更优秀的开发者。
提取类/抽离方法:正如上面提到的,像“臃肿的类”(一个类提供了本该有几个类提供的功能)这种代码异味应该将原有类中的方法和属性移动到适当数目的新类中去。旧类中对应新类的方法和属性应该被移除。另外,有时候一些类过于臃肿是因为它包含了被其他类使用本应该是其他类的成员方法的成员方法。这些方法也应该被迁移到合适的类中。
提取方法:像上面提到的“过长的方法”这种代码异味可以通过从旧方法中提取代码到一个或多个新方法中消除。
分离条件:许多时候,一个方法很长是因为包含好几个分支语句(if-else)。这些分支条件可以被提取和移动到几个单独的方法中。这确实能大大改善代码可读性和可理解性。
引入参数对象/保留全局对象:在我做代码审查时发现另外一个很常见的情况 - 好几个参数被传入方法。问题主要与需要从已有方法中增加或者移除一个方法参数有关。在这种场景,建议将相关方法参数组成一个对象(引入参数对象),让方法传递这些对象而不是每个单独的参数。
用符号常量替换魔法数字:对于有意义的并且到处被使用的字面常量,应该为它们分配一个命名常量。这能大大增强代码可读性和可理解性。
重命名方法:正如上面提到的,模糊不清的方法名会影响代码的可使用性。这些模糊不清的名称应该重命名为有意义的可能与业务术语有关的名称,来帮助开发者通过业务上下文更好地理解代码。这很需要技巧并且要求开发者与业务专家一起协作来理清代码需要满足的业务需求。有趣的是,这种重构方法看起来似乎非常容易理解,但是常常被许多开发者忽视,虽然在Eclipse这种IDE的refactor菜单项中经常出现这一项。
用Sonarqube可以改善代码风格,也容易找出一些简单的bug。
上一篇: Qt SDK 1.2发布
下一篇: 与父亲的小妾私通,揭秘春秋最无耻的君主