我们总是很容易就能写出满足某个特定功能的代码,却很难写出优雅代码。又最欣赏那些优雅的代码,因为优雅代码更能体现一个开发者的积累。
就像写一篇散文,有的就像初学者不得其门而入,遣词造句都非常困难,然后纠纠结结,最终不了了之。或者啰哩吧嗦,看起来说了一堆,其实就像是村妇闲聊,毫无重点,不过是口水文而已。
好代码应该是这样的,如涓涓细流、如同一首诗,一篇优美的故事,将作者编写代码时的情感慢慢铺垫开来,或是高潮迭起,此起彼伏,或是平铺直述,却蕴含道理。我始终相信优秀的代码是有灵魂的,代码的灵魂就是作者的逻辑思维。
编写整洁代码 or 非整洁代码,就像平时生活中是否注意爱护环境的一点点小习惯,一旦坏味道代码没有及时处理,就会成为破窗效应,然后逐渐的代码越写越烂,最终这些代码要么以重构收场,要么就被抛弃。
我们见过太多没有毫无质量可言的代码,许多时候开发者们由于能力原因、或者时间有限,写了许多能够满足当前工作的代码,然后就弃置高阁,不再理会。于是,代码写之前的只有自己和上帝能理解代码的意思,而写完了之后,只有上帝能懂了;还有一些开发者说:我只会写代码,不会优化代码,他们仿佛特别勤奋,每天都会比其他人都热衷于熬工时,但是写出的代码,实际上是一个个难以维护的技术债。而且许多代码的作者总喜欢找各种借口来抵赖,例如喜欢说代码出了问题都是底层框架太垃圾了、或者别人的代码封装得太差。他们总是抱怨这抱怨那,但是即便有优秀的框架、技术,就一定能写出优秀的代码么?
在这里笔者列举了平时看到过一些自认为不太整洁的代码,以及与代码整洁之道中相对应的范例,欢迎大家一起来拍砖。
01命名规则
1.1 变量命名和方法命名
在我们刚刚开始学习写代码的古老时代,或许会有下面这种习惯。
这是一个喜欢用自己的姓名来命名类和方法的作者,在他的代码中,经常可以看到这样奇怪的对象定义,而且他还喜欢用a,b,c,d,e,f或者s1,s2这样的命名,仿佛他的代码自带混淆特效。这样的代码嗅起来会不会觉得充斥着奇怪的味道?
如果一个项目中,有十几个地方都出现了这个 GetData() 方法,那种感觉一定非常难受。
1.2 Model、Dto 傻傻分不清楚
随着技能的增长,或许我们会学到一些新的代码概念,例如,Model、DTO 是经常容易弄混淆的一种概念,但是在某些代码中,出现了下面的命名方式就有点令人窒息了。
这位大概是一位对概念严重消化不良的资深开发者,居然同时把 Model 和 DTO 复用在一个对象上,
ps:当然,一个开发者定义变量的背后一定有他的动机。
他到底是想要的是用来在 MVC 模式解决数据传输和对象绑定的模型对象?还是用于传输数据的 DTO 呢?
其实他定义这个对象,是为了定义存储数据对象的实体( Entity )。
1.3特殊情况术语和字段对照表非常重要
近年来开发者素质越来越高,所以许多优秀开发者会倾向于使用翻译软件来翻译变量名,然后用英语来命名,但是即便如此,许多政务项目总是能嗅出一些奇怪的味道。
例如前不久看到一条这样的短信:(原图已经消失)
xxx公积金中心提醒您:您于{TQSJ}日进行了{TQCZ}操作,账上剩余金额为{SYJE}元。
这是个bug将xxx公积金中心的某些秘密透露在大家面前。作为一个严谨的项目,居然使用中文首字母大写命名法,这让习惯于大驼峰、小驼峰的我看了之后尴尬癌犯了,很不舒服。但是这也是许多政务信息化项目的中字段命名的规范,而且在这种情况下,往往会输出一份非常规范的数据库字段对照表,确保中文和首字母的语义不让人产生歧义。
所以特定语境下,变量和方法本身没有严格的规定,但是一定要使用恰当的语境概念,对于这样的特定场景,尽量维护一份实时更新的术语表吧。
02状态码返回值
2.1业务逻辑状态码
似乎在对外提供接口时,使用下列接口状态码是一种比较常见的惯例。提供统一格式的 code 状态码以及返回的消息和成功返回结果时的填充数据,能够让开发者高效的完成接口对接,无需关心http状态码背后的含义。
2.2用 http 状态码为什么不够?
上面这是一种经典的流派,还有一种流派则会使用http状态码来返回指定的数据,事实上 http 协议本身已经提供了许多状态码,例如下面的这些大家都非常熟悉的状态码。
但是这些状态码为啥不够?主要是为了减少前后端、服务上下游之间接口对接的难度,也是一种提高效率的方式。但是 http 状态码是一种通用的格式,应尽量使用这种方式,而不应该通过解析正常响应后的 json 来判断是否正确操作。
03switch 语句与判断语句
3.1 面向过程式或面向对象式 我曾经跟小组中一位大佬交流他的一段代码,他的这段代码大概是这样的。
读者卒...
且不说这位大佬的代码是写得好或者不好,仅仅就这200多行代码的4个大switch读起来大概会让人便秘难受吧。于是在我读完这段代码之后,我冒死向他请教这么写代码的原因,他说我这个流程处理就是一个简单的用例场景,哪里还有什么可以优化的余地?
我跟他介绍了20分钟代码封装的必要性,于是,他把代码写成了这样。
这酸爽令人简直难以置信。(事实上这个新鲜出炉的遗留应用,正是这样一点点堆积了许多总代码行超过千行的类文件)
Robert 大叔给出了这样的建议:
对于switch 语句,我的规矩是如果只出现一次,用于创建多态对象,而且隐藏在某个集成关系中,在系统中其他部分看不到,就还能容忍。当然也要就事论事,有时我也会部分或全部违反这条规矩。
上文我给出的示例,有点像面向过程的代码风格,而 Robert 大叔在他的书中写下的示例是这样的(抽象工厂模式的示例)。
这清爽的感觉,让人很舒服啊。
3.2 孰优孰劣?
当然,原示例是一个流程处理的例子,似乎大家的流程处理代码都习惯于使用这种面向过程风格的写法,反正要加判定条件,就加一个 case 就可以了。
而在某些特定情况下,甚至用 if / else 来写逻辑判断更简单,于是我们经常在某些销量很好的快速开发平台中,看到这样的例子。
这些典型的面向过程风格的代码,确实读起来似乎更加简单、而且也易于实现。
Robert 大叔是这样说的:过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数,面向对象代码便于在不改动既有函数的前提下添加新类。
反过来讲也说得通:过程式代码难以添加新数据结构,因为必须修改所有函数,面向对象代码难以添加新函数,因为必须修改所有类。
所以究竟是使用面向过程式代码,还是面向对象式代码?没有万试万灵的灵丹妙药。
04奥卡姆剃刀定律、得墨忒耳律
4.1“如非必要,勿增实体”
一旦开始初步掌握面向对象开发的基本原则,于是我们就会新建许多各种不同的模型对象。尤其是在webapi接口开发过程中,更是如此。
切勿浪费较多东西去做,用较少的东西,同样可以做好的事情。
4.2 得墨忒耳律
假设有一段代码是这样的。
会不会为了获得某些数据,而写出这样的代码呢?
这样就是典型的对得墨忒耳律的违背。这个原则指出:
模块不应了解它所操作对象的内部情形。
更准确的说,得墨忒耳律认为,类C的方法f只应该调用以下对象的方法:
C(本身)
由方法f创建的对象。
作为参数传递给f的对象;
由C的实体变量持有的对象。
对象不应调用由任何函数返回的对象的方法。换言之,只跟朋友说话,不与陌生人说话。
祖父只跟自己的亲儿子(Father)说话,而不跟孙子(Me)说话。
05圈复杂度
在软件测试的概念里,圈复杂度用来衡量一个模块判定结构的复杂程度,数量上表现为线性无关的路径条数,即合理的预防错误所需测试的最少路径条数。圈复杂度大说明程序代码可能质量低且难于测试和维护,根据经验,程序的可能错误和高的圈复杂度有着很大关系。
据说在Oracle数据库中有一些屎山代码,是通过一堆标识量来判断某些特定逻辑的,大概是这样的。
这是一个圈复杂度非常复杂的方法,我想任何一个读到这样代码的开发者都会对自己的人生充满了积极而乐观的判断,那就是“活着比一切都好”。
对于这样的代码,我们应该尽可能的降低代码的圈复杂度,让程序满足基本可读的需求。
06注释
我曾经参加过一个使用objectc编写的应用的,其中有一段代码是这样的,这个flag大概是魔法值,作者未经考证直接就在代码中使用了。然后一直流传下来,成为一段佳(gui)话(hua)。
还有这样的注释。傻傻分不清楚。
还有这样的。
Robert大叔如是说:
什么也比不上放置良好的注释来得有用。什么也比不会乱七八糟的注释更有本事搞乱一个模块。什么也不会比陈旧、提供错误信息的注释更有破坏性。
当然很多中国程序员自称其变量命名是自注释的,例如大概是这样的。万能的 Is 命名法,只要是判断状态皆可用。
ps:每个程序员能够成功的生存下来都不容易,他一定有异于常人的本事。
07霰弹式修改
CRUD开发者或许经常会看到这样的代码,例如,如果我要对某一个对象的状态( Status)进行更改,可能会这么做:
这种霰弹式代码中,一处代码规则的变化,可能会需要对许多处代码进行同步修改,使得我们的代码异常的难以维护。
08异常
有时候可能会遇到这样的代码,在方法中定义一些文本的状态码,然后调用方法时,再去判断这个状态码的内容,当返回错误码时,要求调用者立即处理错误。
不如直接抛出异常,让异常处理机制进行处理吧。
09边界
9.1 模块间的边界
即便是简单的CRUD应用系统,优秀的开发者也能更好的处理应用程序模块间的边界。某种意义上讲,应用程序内部的边界看起来或许没有明确的界限之分,但是稍不留心就可能导致应用程序间关系过于紊乱,让其他开发者捉摸不透。
例如,假设有一段代码是这样的,在用户操作类中,加入了一个获取应用数据的方法,确实会让人很费解吧。
9.2应用间的边界
相对而言,或许应用间的边界似乎能相对清晰的分析出来?并非如此。
在当今时代,我们很少开发完全与其他应用系统没有任何关联的独立软件,这意味着我们或许无时无刻都得与其他第三方应用进行接口行为或数据的交换。这让我们必须确保采取措施让外来代码干净利落地整合进自己的代码中。
在代码整洁之道书中,Robert大叔推荐应该第三方接口进行隔离,通过Map那样包装或者使用适配器模式将我们的接口转换成第三方提供的接口。让代码更好地与我们沟通,在边界两边推动内部一致的用法,当第三方代码有改动时修改点也会更少。
10总结
写代码是开发者的基础技能,无论你是.NET 开发者,或者C/C++ 开发者,你都在努力用代码实现自己的梦想。如同韩磊老师在译作代码整理之道封面上总结全书,写下的那句诗
“细节之中自有天地,整洁成就卓越代码”。
卓越代码从来不仅仅只是功能完善、代码齐全,做好细节,每个细节就是一方小天地。优雅的代码,不仅仅只是开发者的个人能力的体现,更是开发者的立足之本。努力改善坏习惯,提高代码质量,时刻消除异味,时刻提高自己,更是个人技能的全面发展的必然要求。
“我是一名从事了10年开发的老程序员,最近我花了一些时间整理关于C语言、C++,自己有做的材料的整合,一个完整的学习C语言、C++的路线,学习材料和工具。C/C++、编程爱好者的聚集地 <进入下方专栏即可看到及领取>!欢迎初学和进阶中 的小伙伴。希望你也能凭自己的努力,成为下一个优秀的程序员。工作需要、感兴趣、为了入行、转行需要学习C/C++的伙伴可以跟我一起学习!”
关注我的专栏,带你遨游代码世界!C语言/C++进阶之路 - 专题 -
最后分享一张C/C++学习路线图给爱学习的小伙伴们