如何保证代码质量
本文最后更新于:2022年8月17日 凌晨
当下,小到手腕上的手环,大到运载火箭,都依赖于使用代码描述的逻辑而运转。代码质量关系着生活的方方面面,如果一段质量不佳的代码被用到了关键位置,就有可能带来严重后果和经济损失,比如阿丽亚娜5运载火箭在首次测试发射时就因此发生事故。
因此,保证代码质量是一件重要的事情,高质量的代码才能带来优秀可靠的项目。
什么是优秀的代码
优秀的代码不少见,它们虽然各不相同,但总有一些共同的优点:
- 函数、变量具备明确的语义,甚至不需要注释很容易就能看懂其作用
- 满足各种边界条件,代码功能正确
- 在功能变更时只需要进行较小的改动,具备较好的拓展性
- 在发生异常时能够优雅的报错,或者能够自我恢复
- 对于未预见的情景,能够优雅处理而不崩溃
- 代码安全度高,不易被攻击
代码的可维护性原则
面对一个庞大的项目,如果代码缺乏可维护性,那么项目未来的发展将会异常艰难(比如一个Bug光是定位问题位置可能就需要非常久的时间),而对项目的更新迭代也将如噩梦般痛苦(面对一大坨不知所云的代码不知所措)。
代码的可维护性原则包括下面几点:
- 统一的编码规范
- 包括命名规范、代码格式、注释规范...
- 比如:不要在命名中混合拼音和英文
- 稳定的工程结构
- 目录清晰(可以轻松找到相关代码)、模块化、组件化、依赖可控(依赖项容易添加和更新,基本保持稳定)、访问权限(防止代码被无意中破坏)...
- 优秀的方案实现
- 文档、用例(对应用场景考虑充分)、可测性(自动化测试)、高内聚低耦合...
代码清晰/复杂程度的度量方法
为了方便度量代码的清晰程度,这里介绍一下圈复杂度(Cyclomatic Complexity,也叫条件复杂度)的计算方法。
根据代码得到CFG(控制流图,Control Flow Graph,是一个过程或程序的抽象表现,是用在编译器中的一个抽象数据结构,由编译器在内部维护,代表了一个程序执行过程中会遍历到的所有路径),则这段代码的圈复杂度=CFG边数-CFG顶点数+2。另一种计算方法对人类更加友好,只需要统计代码中判定条件的数量,圈复杂度=判定条件数+1,这两种计算方法是等价的。
有了圈复杂度,我们就可以对代码进行评估了,通常来说,如果圈复杂度为1~10,那么代码是清晰的,维护成本也较低;而如果圈复杂度大于30,那么代码就基本不可读,维护成本也非常高。在实际应用中,单个文件的圈复杂度最好不要超过15。
代码评审 Code Review
代码质量关系着项目的未来发展和维护,那么为了保持代码的质量,就需要对代码进行评审。代码评审(Code Review,CR)是通过阅读源代码,检查代码是否符合编码规范以及前置发现代码质量问题。
CR的关注点
CR需要重点关注:
- 代码规范
- 命名:变量名、函数名、类名是否清晰准确
- 描述:提交的描述是否准确对应到功能
- 风格:代码风格是否遵循统一规范
- 文档:是否提交/变更了相应的文档
- 功能设计
- 设计:设计是否良好、是否适合当前系统
- 功能:实现是否正确
- 简洁:实现是否简洁,有无更好的实现方式
- 测试:是否包含自动化测试,代码可测性如何
小CL(Change List)的好处
前面介绍了CR是什么以及CR关注什么,下面来介绍小CL的好处。小CL是指提交进行Code Review的Change List要尽可能小,把一大段代码拆小再提交给CR。这样做的好处是审查将会更快(代码片段小,不需要太长时间)、更彻底(代码少,更容易看懂),更容易合入代码(合并时不会产生大的冲突),此外如果未能通过代码审查而被拒绝,也能快速修改。
如何面对CR的不同意见
- 正视批评
- 当审查者对代码提出批评时,应当正视批评,不要为批评而倍感沮丧
- 修复代码
- 如果审查者对代码的某一部分产生误解,应当及时澄清代码并加上注释
- 自我反思
- 思考自己的解决方案是否合理,是否有更佳的解决方案
- 解决冲突
- 尝试在基于技术事实和业界标准的基础上与审查者达成一致
代码重构
在很多时候,我们可能无法在项目的一开始就确定最佳的架构、最优的技术,可能会因为种种原因而导致代码质量没有那么高,也就是产生了所谓技术债(Technical Debt),给未来的项目维护带来了负担。常见的技术债产生原因有时间紧迫、技术水平不够、业务压力大等。既然是技术债,总归是要偿还的,解决技术债的一种方法就是对代码进行重构,以提升代码的可维护性。
代码重构是指对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
何时重构
重构,其实就是为了提高代码质量,不断对抗所谓的Code Smell(代码的坏味道)。因此,当出现Code Smell时,就应该进行代码重构了。常见的Code Smell有:
- Duplicated Code 重复代码
- Dead Code 死代码,代码没有什么用,但是却没有被移除
- Long Method 冗长的方法体
- Lazy Class
- Feature Envy
- Divergent Change
- Refused Bequest
- Shotgun Surgery
- Inappropriate Intimacy
- ......
常见的代码重构方法
- 对于重复代码,可以抽离出公共代码,进行类型一般化(从几个类中抽取出公共部分作为这几个类的父类)
- 对于参数过多的函数,可以将参数封装,通过函数调用获得参数(比如将几个关系较近的参数放在一个类中,传参时传入该类的对象,函数通过调用相关方法获得参数)
- 对于过于冗长的函数,可以从中抽取出多个子函数,而不是把所有代码堆在一个函数中
- 对于过于庞大的Switch-Case语句,可以通过策略模式、类型抽象来重构(把Case抽象为一个个类,并为这些类抽象出同一个方法,各个类通过重写方法来实现Case相关的代码,这样只需要调用这个方法而不是写一堆Case语句)
- 对于嵌套的条件分支,可以提取方法来降低圈复杂度(把嵌套的条件取出来)
- 对于长调用链(比如
a.b().c().d()
),可以通过隐藏中间人调用来解决(比如把d()
直接提取到a
中,实现a.d()
) - ......