提升&保持代码质量的一点心得

Posted by lx8421bcd on June 3, 2019

前言

前两天在知乎写了一篇回答,关于如何平衡代码洁癖和项目进度,算是以一个项目组普通开发的角度讲了一下如何提升多人合作项目的代码质量。一边写一边回顾自己从开始学习编程以来的项目经历,逐渐觉得其实还可以再写细致一点,结合个人项目和多人合作项目来理一理,不过为了避免回答扯太远,就懒得改了,在这里写一写好了。

为什么要去提升代码质量?将就着干不好么?我觉得这是一个短期收益不明显,但长期收益显著的事情。

首先一个项目你维护的越久,提升代码质量对你的好处就越大,最直观的好处就是你后续开发的时间成本、因为踩坑而产生的加班越来越少,但这一点在你一次两次重构的时候是不明显的,我就见过身边不少人抱着“这烂代码又不是我写的,我为什么要管它”的心态捏着鼻子写项目,越写心态越爆炸,陷入恶性循环:

垃圾代码不想看 -> 工作要做捏着鼻子看 -> 越看越不想看 -> 消极怠工,需要花更多的时间看

最后就是忍不了跑路。

从另外一个角度说,主动去提升代码质量十分有助于提升你的工程能力,重构本身就是一门学问,花时间重写谁都会,但是如何花最少的时间,达到这个时间允许内最好的效果,把代码质量一点点提升起来,保持下去,则需要相当的水平。

哪怕从最功利的角度来说,面向离职编程,你希望你在跑路的时候简历上写的是“参与了XXX项目的XX、XX模块的开发”呢,还是“参与了XXX项目的XX、XX模块开发,期间逐步重构,优化代码,代码行数减少XX,性能提升了XXX”呢?后者很显然给了面试官提问的切入点,而问的东西你做过,也容易聊起来,更方便展现自己。

基本要求

个人认为按每种编程语言的规范来写代码是写工程项目最基本的要求,包括命名、缩进等规范。无论你的技术水平如何,都应该在写代码前三思而后行,按照使用语言规范,按照命名规范来命名变量和函数,遇到自己没有把握的命名,先去查词典,如果查出来的结果仍然不确定,再去相似的中/英文网站上看看别人怎么命名;每一个函数、类型的编写都尽量做到封装完善等等。通用的变成规范和注意事项有很多书籍和文章讨论,我觉得这里没必要过多的叙述,我推荐点相关的书籍和文章好了

可能你会发现到身边有些人对此不以为然,觉得这是“小节”,不拘小节嘛,不这样做代码质量也不会降低多少。但我认为这是程序员的基本素质,随意对待代码的人,一般技术也不会有多强。就像枪法之于士兵,现代战争下轻武器的作用确实很低了,枪法辣鸡,跟着大部队推进摸鱼确实不会怎么样,但你让他执行特种作战任务,他就不行,强行上一般都会出事。

一个人随意的时候,想法是多变的,可是一旦他认真起来,基于其认知水平的逻辑推理和最后的结论往往长期不会改变,所以如果在编码时总是尽自己所能写出最高质量的代码,长期下来代码风格就会开始固定,回顾代码时就基本上可以做到快速明白这段代码的逻辑,“我当时为什么这样写”这个问题在代码风格固定之后就很难出现。这一点,就是保持代码质量的基石。

过早优化乃万恶之源

这是软件工程领域的一句名言了。

需求说,给应用加上更新需要写一个下载安装包的功能,于是你开始了自己的宏图大业:后台下载、速度控制、断点续传、下载队列、进程保活……
而实际上需求只是要求你实现最简单的下载而已,不错,你写的功能是很有用、对于体验是有提升,但是这不是为了一个简单的下载需求所准备的。你所需要付出的工期与需求并不匹配。而到真需要用这些功能的时候,可能会用更成熟、更完善的下载框架取而代之,换而言之,你脑子里构思的这一套东西,如果真的落实下来,可能毫无有用武之地,白白浪费了开发成本。

简而言之一句话“有多少需求干多少事”,保持基础接口不变,方便后续扩展即可。以上面的更新为例,就先按照需求规划用一个简陋的就行了,后期当应用复杂起来,下载需求高了再用成熟的框架替换下载实现即可。只要启动更新接口对于调用者来说不变,那么交给谁下载其实是个无关紧要的问题。

需求为大?

这可就是我的亲身经历了,初来实习的时候,公司里就我一个客户端开发,部门老大还让我做商城app1.0版本(从零开始),刚开始实习那一个月,真的是每天都在头疼架构怎么设计,天天加班,以至于HR总监打电话来问是什么情况。
过进度的时候说起这事,部门老大说,管那么多干嘛,先实现了再说。后来写到心态爆炸放飞自我,最后晚了一个星期交货,购物车、结算、订单管理这些复杂模块写的一塌糊涂。结果部门老大倒不是很在乎,有东西就行(后来我才知道他是急着拿东西去邀功)。CTO看过我的代码之后表示整体还行,有些暇疵。不过代码怎么样我自己心里还是有B数的,后续版本迭代的时候,这些放飞自我的代码基本一行没留,不然不知道得踩多少坑。

需求为大,这句话没错,是工作中的真理,一切都要以实现需求为第一优先级。但很多人忽略了一点,这句话是给熟练工说的。

当你还没本事随手写出质量达标,结构稳定的代码时,哪怕需求大过天,也不能放飞自我,一定要尝试写出自己当前水平下质量最高的代码。每一次放飞自我都是在给之后挖坑,谨记破窗效应。

渐进式提升

任何质量控制、重构,都必然是渐进式的,也就是必然有0%~100%的过程,妄图一步登天则必然失败。
当一个项目的骨架优化完成,进度基本上就达到了50%,所谓的骨架,及项目架构,基础模块。他们为业务代码提供支撑,方便业务快速开发。如何优化它们,文章下面两段有叙述。
骨架完成后,一般可以保证新业务代码的质量,旧的代码木已成舟,慢慢修改也无伤大雅,可以利用空余时间来做,也可以专门排期来做,最好的是利用业务变更的契机去重构。

不要重复造轮子

这是一个老生常谈的问题了,简而言之,遇到什么需求时,先搜索一下是否有成熟的解决方案,一个看似功能简单的组件背后,可能是数位/数十位开发者多年的打磨。你可以引入框架之后研究、优化、扩展,但绝大多数情况下,重新写只会写出一个更为简陋问题更多的东西来。

“我们组装的木船需要发动机,顺手自己造一台吧”

如果你觉得这句话很荒谬,那么请记住多数时候自己造轮子跟这个一样荒谬。

注重架构

“架构”指的是软件结构设计,诸如模块、组件、分层等,本质上来说是对代码组织方法的抽象化表达;是做任何软件项目都必须面对的问题。然而我发现市面上大多数书籍和教材,很少讲这方面的问题,特别是针对初学者、小项目应该如何组织代码。
由于一些话术的问题,国内很多人谈起“架构”这个词就自动联想到infoQ一堆高端架构师展示的国内巨无霸系统的架构,架构这个词好像离普通人很远。然而我们做小项目就没有架构了么?大公司造摩天大楼需要建筑师设计结构,我们盖二层小楼就可以随便堆砖头?你看,换成房子来说一下子就说不通了。
当一个项目拥有好的架构的时候,代码质量不会差到哪里去。框架层面能解决的事情越多,业务层面需要干的活就越少,在一个优秀的代码组织框架下,遵循框架来扩展业务,顺着框架写,是阻力最小的。特别是对于多人合作项目来说,架构设计可以起到引导、限制别人的作用,当业务实现有范式可以参考的时候,没有人会想自己玩点花样,复制粘贴范式来改一改自然是最优的选择。这一点就可以以我维护的一个Android项目为例:

我刚来这个组的时候,组长比较忙,项目架构就是最基本最原始的架构,组里剩下几位技术水平也不怎么地,他们想引入一些第三方SDK简化开发(比如ButterKnife,EventBus)但是老大两句话就能给他们问住了,一直也没能引进来(老大没时间研究新库,但是也不抵制使用,意思就是,你要用可以,但是得给我讲讲利弊,你的掌握程度,免得引进来没人踩坑)。就这水平自己设计一些组件、自定义控件肯定也是没戏的。应用里一些复杂页面的通信,比如嵌套fragment,还在用接口互调的方式来做,内存泄漏,空指针,发生的频率很高。另外不得不提的一点就是哪怕是分页加载这种客户端列表最常见的功能也会有好几种写法,看的很头疼。
我来了以后首先说服组长引入了EventBus,在一次页面重构的需求中彻底铲除了之前盘根错节的接口互调。事实上在EventBus引入之后,项目组里其他人再写组件、页面间的通信,也不会再去写接口互调了,毕竟这种写法相比于EventBus真的是麻烦100倍,需求变了还难得改。
在后来一次新需求的时候,我引入了自己开源项目里的分页计数器和封装好的分页加载工具,只用声明、传入分页数据数量判断是否有下一页,后面的多次改版、重构中普及这种写法普及到很多页面上,到后期基本完全替代掉了以前乱七八糟的分页加载写法。

注重复用

对于一些重复性的需求,可以编写工具、组件来实现复用,这个逻辑其实跟上面框架限制也是一样的,从某方面来说,程序员都是比较懒的,有成品用的时候,没人愿意费脑子再换个花样写一遍。注重提升复用度,在提升代码质量方法和手段中可以排在前三。

我们都知道“don’t repeat yourself”,除非特殊需求,处理同样逻辑的代码不应该多次出现于一个工程,复制粘贴一时爽,逻辑修改火葬场,同样逻辑的多份代码存在于一个项目,那么逻辑变更之后就要改多处。举例来说,某些接口的手机号验证,使用正则表达式判断,验证一次,复制粘贴一次,那时候还没有170号段的虚拟手机号,现在要求支持,是不是得改很多地方?假如逻辑复杂点,不仅支持虚拟手机号,还要支持海外手机号,改起来又要多麻烦呢?相反,假如一开始就坚持将手机号验证逻辑封装成一个函数或者功能模块,只需修改这个模块就能够变更工程内所有手机号的认证逻辑,另一方面由于实现统一在这一个模块,也方便针对这一逻辑编写单元测试。相较于复制粘贴的维护成本来说,单独整这一个模块的开发成本真的不算什么。

我在那个知乎回答中也提到过,对于一个刚参加工作的新人来说,在工作中尝试整理相似的逻辑,编写工具和模块统一处理,是一种很好的提升能力的方法,同时也是一种很好的展现自己的方法。这并不是要求你拿一份的钱干两份的活,相反这些工作工程量并不大,在做安排给自己的需求的时候就可以完成,在之后的需求中逐步推广到整个工程,这是一种主导能力和工作态度的展现;往低了说哪怕这些工作不会给你带来什么绩效,从长期来看起码能降低你开发项目的麻烦程度,也是好事一件了。

谨慎追新,不要炫技

软件设计有两种方法:一种是设计得很简单,明显没有缺陷;另一种是设计得很复杂,没有明显缺陷。第一种方法要困难得多。 – Tony Hoare

我在学习和工作中遇见过一类人,他们特别喜欢追逐新技术,其常见话语为:“快看AAA,XXX公司出的,好牛逼,赶快学,再不学要被淘汰了!”
这个AAA可以是新架构,比如MVP,MVVM;可以是新SDK,比如RxJava,Android X;也可以是新的开发平台,比如RN、Flutter。
他们是如此的害怕被淘汰,如此迫切的希望能在新技术上有实战经验,以至于自己还没研究透彻,就要在项目上实践。
结果,自然是一地鸡毛……

正如我在段落开头引用所言,想把复杂的东西写的很简单,是需要很高的水平的,最起码你得熟练使用,知道哪里该用。当你还在想show一把的时候,大概率是还没有完全掌握熟练的,就像小朋友刚学了个潮词就什么地方都要用一下。

拿小的来说,我一同事负责一个需求,任务是优化首页加载速度,以前首页5~6个接口一并请求,加载略卡,现在要优化为依次请求,但是其中一个接口请求失败不影响其他接口请求。其实最简单的套路就是写嵌套调用,无论异常与否都请求下一个接口。可我同事刚学RxJava,很想秀一把,先试了一下zip操作符,好像不行,又试了一下merge操作符,好像也不行,白忙活了一天,最后老老实实的加班用嵌套写法写了(其实应该用mergeArray,另一方面需要规划好如何统一处理返回,这是重点,如果不能统一处理,那还不如嵌套)。这个还好,虽然小加了一班,但是没影响到项目进度。

稍微大点的,一学弟,大三的时候帮老师做一个Android外包项目,他第一次碰上从零开发一个项目,没什么架构经验,人又比较好学,研究了一下之后就决定用MVP架构搭建项目。项目不大,本来计划工期一个半月,但由于他这个决定,不得不一边写项目一边踩坑,磕磕碰碰的写了3个月才交付。最后交付的还是一个“形式上”的MVP架构,空有MVP的壳没有MVP的神,我Review他代码的时候很多地方完全看不出MVP的意义在哪,说不好听点,就是多花了一个半月的时间用一个不成熟的架构给以后挖坑。

追逐新技术,高明的写法,保持自己学习的兴趣和动力,值得赞赏。但我们毕竟是在做工程,做工程要严谨,特别是多人合作的工程。你上新技术,能带来怎样的好处?会引入新问题么?注释写足了么?文档准备了么?都答不上来?只是比较cooooool?

那就真的不要抱怨老大古板了……

总结

以上所述的几点,基本以非管理者的视角,总结在个人项目与工作中提升与保持代码质量的要点和方法。
总而言之,不需要将保持代码质量看作是很复杂需要毅力去坚持的杂活,它本身就是一种习惯,刚开始改变时会感到麻烦和困难,但当你周围的环境因坚持习惯而逐渐改变的时候,就会享受到它带来的益处。
当我们自己做项目或者主导项目时,应该去订立规范,令行禁止。
当我们参与项目时,当一个积极分子,尽自己所能提升项目的代码质量。这对于个人发展和公司项目都是有好处的,可以说是双赢。
其实我说的这些,相信熟练工们应该多少都有点感觉,不过对于还在迷惑中的同学来说,如果这些碎碎念能帮到你,那是最好不过了。