最近趁着学习劲头足,如饥似渴的把代码重构部分也学习完了,可以说9月份属实非常充实,貌似也只有学习才能导致生活没有那么无聊。
代码重构博客目录
文章的结构如下
代码重构重点内容
对这部分的学习内容做个小结
重构的目的、内容、时机、方法
什么是重构
重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低,这段定义可以理解为在保持功能不变的前提下,利用设计思想、原则、模式、编程规范等理论来优化代码,修改设计上的不足,提高代码质量,其中提高代码质量也就是提高代码的 可读性、可扩展性、可维护性、可重复性、简洁性、灵活性、可测试性
重构的目的,为什么重构
- 对于项目来言,重构可以保持代码质量持续处于一个可控状态,不至于腐化到无可救药的地步。
- 对于个人而言,重构非常锻炼一个人的代码能力,并且是一件非常有成就感的事情。它是经典设计思想、原则、模式、编程规范等理论知识的练兵场
重构的内容
根据重构的规模,可以笼统地分为大规模高层次重构(以下简称为“大型重构”)和小规模低层次的重构(以下简称为“小型重构”)
- 大型重构指的是对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入 bug 的风险也会相对比较大
- 重构的手段:分层、模块化、解耦、抽象可复用组件等等。
- 重构的工具: 设计思想、设计原则和设计模式。
- 小型重构指的是对代码细节的重构,包括类、函数、变量等代码级别的重构,比如规范命名、规范注释、消除超大类或函数、提取重复代码等等。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入 bug 的风险相对来说也会比较小
- 重构的手段:编码规范。
大型重构需要系统化的解决,小型重构自己平时随见随改
重构的时机
建立持续重构意识,把重构作为开发必不可少的部分,融入到日常开发中
重构的方法
大规模高层次的重构难度比较大,需要有组织、有计划地进行,分阶段地小步快跑,时刻保持代码处于一个可运行的状态。而小规模低层次的重构,因为影响范围小,改动耗时短,所以,只要愿意并且有时间,随时随地都可以去做
重构的保障:单元测试,以及如何提高代码可测试性
关于单元测试和代码可测试性的核心内容
什么是单元测试
单元测试是代码层面的测试,用于测试编写的代码的逻辑正确性。单元测试的测试对象是类或者函数,用来测试一个类和函数是否都按照预期的逻辑执行。这是代码层级的测试
写单元测试的好处
单元测试能有效地发现代码中的 Bug、代码设计上的问题。写单元测试的过程本身就是代码重构的过程。单元测试是对集成测试的有力补充,能帮助我们快速熟悉代码,是 TDD 可落地执行的折中方案
如何编写单元测试
写单元测试就是针对代码设计覆盖各种输入、异常、边界条件的测试用例,并将其翻译成代码的过程。可以利用一些测试框架来简化测试代码的编写。对于单元测试需要建立以下正确的认知:
- 编写单元测试尽管繁琐,但并不是太耗时;
- 可以稍微放低单元测试的质量要求;
- 覆盖率作为衡量单元测试好坏的唯一标准是不合理的;
- 写单元测试一般不需要了解代码的实现逻辑;
- 单元测试框架无法测试多半是代码的可测试性不好。
单元测试为何难落地执行
- 写单元测试本身比较繁琐,技术挑战不大,很多程序员不愿意去写。
- 研发比较偏向“快糙猛”,容易因为开发进度紧,导致单元测试的执行虎头蛇尾
- 没有建立对单元测试的正确认识,觉得可有可无,单靠督促很难执行得很好。
什么是代码的可测试性
粗略地讲,所谓代码的可测试性,就是针对代码编写单元测试的难易程度。对于一段代码,如果很难为其编写单元测试,或者单元测试写起来很费劲,需要依靠单元测试框架很高级的特性,那往往就意味着代码设计得不够合理,代码的可测试性不好
编写可测试性代码的最有效手段
依赖注入是编写可测试性代码的最有效手段。通过依赖注入编写单元测试代码的时候,可以通过 mock 的方法将不可控的依赖变得可控,这也是编写单元测试的过程中最有技术挑战的地方。除了 mock 方式,还可以利用二次封装来解决某些代码行为不可控的情况。
典型的、常见的测试不友好的代码
代码中包含未决行为逻辑;滥用可变全局变量;滥用静态方法;使用复杂的继承关系;高度耦合的代码。
大型重构的手段:高内聚,低耦合
关于解耦的相关核心内容
解耦为何如此重要
过于复杂的代码往往在可读性、可维护性上都不友好。解耦,保证代码松耦合、高内聚,是控制代码复杂度的有效手段。如果代码高内聚、松耦合,也就是意味着,代码结构清晰、分层、模块化合理、依赖关系简单、模块或类之间的耦合小,那代码整体的质量就不会差。
代码是否需要解耦
- 间接的衡量标准有很多,比如:改动一个模块或类的代码受影响的模块或类是否有很多、改动一个模块或者类的代码依赖的模块或者类是否需要改动、代码的可测试性是否好等等。
- 直接的衡量标准是把模块与模块之间及其类与类之间的依赖关系画出来,根据依赖关系图的复杂性来判断是否需要解耦重构
如何给代码解耦
给代码解耦的方法有:封装与抽象、中间层、模块化,以及一些其他的设计思想与原则和设计模式,比如:单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特法则。设计模式比如观察者模式
小型重构的手段:规范的十五条军规
关于编程规范的核心内容
命名与注释
- 命名的关键是能准确的达意。对于不同作用域的命名,可以适当的选择不同的长度,作用域小的命名,比如临时变量等,可以适当的选择短一些的命名方式。除此之外,命名中个也可以使用一些耳熟能详的缩写。
- 借助类的信息来简化属性、函数的命名,利用函数的信息来简化函数参数的命名。
- 命名要可读、可搜索。不要使用生僻的、不好读的英文单词来命名。除此之外,命名要符合项目的统一规范,也不要用些反直觉的命名。
- 接口有两种命名方式。一种是在接口中带前缀"I",另一种是在接口的实现类中带后缀“Impl”。两种命名方式都可以,关键是要在项目中统一。对于抽象类的命名,更倾向于带有前缀“Abstract”。
- 注释的目的就是让代码更容易看懂,只要符合这个要求,你就可以写。总结一下的话,注释主要包含这样三个方面的内容:做什么、为什么、怎么做。对于一些复杂的类和接口,可能还需要写明“如何用”。
- 注释本身有一定的维护成本,所以并非越多越好。类和函数一定要写注释,而且要写的尽可能全面详细些,而函数内部的注释会相对少一些,一般都是靠好的命名和提炼函数、解释性变量、总结性注释来做到代码易读。
代码风格
代码风格都没有对错和优劣之分,不同的编程语言风格都不太一样,只要能在团队、项目中统一即可
- 类、函数多大才合适:对于函数代码行数的最大限制:最好不要超过50行;对于类的代码行数的最大限制:当一个类的代码读起来让你感觉头大了,实现某个功能时不知道该用哪个函数了,想用哪个函数翻半天都找不到了,只用到类的一个小功能要引入整个类(类中包含很多无关此功能实现的函数)的时候,这就说明类的行数过多了
- 一行代码多长最合适,一行代码最长不能超过 IDE 显示的宽度。需要滚动鼠标才能查看一行的全部代码,显然不利于代码的阅读
- 善用空行分割单元块,对于比较长的函数,如果逻辑上可以分为几个独立的代码块,在不方便将这些独立的代码块抽取成小函数的情况下,为了让逻辑更加清晰,除了用总结性注释的方法之外,还可以使用空行来分割各个代码块
- 类中成员的排列顺序,常用的排序规则如下:
- 在类中,成员变量排在函数的前面。
- 成员变量之间或函数之间,都是按照先静态(静态函数或静态成员变量)、后普通(非静态函数或非静态成员变量的方式来排列
- 成员变量之间或函数之间,还会按照作用域范围从大到小的顺序来排列,先写 public 成员变量或函数,然后是 protected 的,最后是 private 的
编程技巧
掌握一些使用的编程技巧
- 把代码分割成更小的单元块,大段的代码逻辑会让阅读代码的人不至于迷失在细节中,要善于将代码拆分,需要注意
- 写代码时要有模块化和抽象思维,善于将大块的复杂逻辑提炼成类或者函数,屏蔽掉细节,这样能极大地提高代码的可读性。
- 只有代码逻辑比较复杂的时候,才建议提炼类或者函数。毕竟如果提炼出的函数只包含两三行代码,在阅读代码的时候,还得跳过去看一下,这样反倒增加了阅读成本。我就非常容易在这里矫枉过正。
- 避免函数参数过多,函数包含 4 个以内参数的时候还是能接受的,大于等于 5 个的时候,会影响到代码的可读性,解决方法有两种
- 考虑函数是否职责单一,是否能通过拆分成多个函数的方式来减少参数
- 将函数的参数封装成对象,这样可以动态添加参数,如果函数是对外暴露的远程接口,将参数封装成对象,还可以提高接口的兼容性,不需要改接口定义,只改对应参数
- 勿用函数参数来控制逻辑,不要在函数中使用【布尔类型的标识参数】或者【根据参数是否为 null】来控制内部逻辑,true 的时候走这块逻辑,false 的时候走另一块逻辑,这明显违背了单一职责原则和接口隔离原则。建议将其拆成两个函数,拆分之后的函数职责更明确,不容易用错,可读性上也要更好。
- 鉴于大多数场景使用标识是因为可能按照标识拆分后的两个函数大多数代码会重复,所以可以将这部分重复逻辑抽取为一个单独的函数,这种解决方式比标识更好一些。
- 函数设计要职责单一,SRP针对的是类、模块这样的应用对象。实际上,对于函数的设计来说,更要满足单一职责原则。相对于类和模块,函数的粒度比较小,代码行数少,所以在应用单一职责原则的时候,没有像应用到类或者模块那样模棱两可,能多单一就多单一
- 移除过深的嵌套层次,嵌套最好不超过两层,超过两层之后就要思考一下是否可以减少嵌套。过深的嵌套本身理解起来就比较费劲
- 解决嵌套问题最好的方式就是校验前置,也就是防卫式编程,使用编程语言提供的 continue、break、return 关键字,在不满足条件时退出,而不是满足条件时继续
- 学会使用解释性变量,例如常量取代魔法数字;解释性变量来解释复杂表达式,也就是把复杂判断条件总结为带有判断目的命名的变量
如何进行代码重构
代码重构的关键知识,重构主要是先进行代码诊断再进行代码重构,重点关注诊断List
常规CheckList
- 目录设置是否合理、模块划分是否清晰、代码结构是否合理,整体结构考虑OK么?
- 是否遵循经典的设计原则(SOLID、DRY、KISS、YAGNI、LOD)、设计思想(封装、继承、抽象、多态、控制反转、高内聚-松耦合、基于接口而非实现编程,多用组合少用继承),设计模式是否应用得当,是否有过度设计,有明显违反理论的地方么?
- 代码是否容易扩展,如果要添加新功能,是否容易实现,好改么,好加功能么?
- 代码是否可以复用,是否可以复用已有的项目代码或类库?是否有重复造轮子(违反DRY),能抽出来么,通用么?
- 代码是否容易测试,单元测试是否全面覆盖了各种正常和异常的情况,好测试么,mock难度高么?
- 代码是否易读,是否符合编码规范(比如命名和注释是否恰当、代码风格是否一致等),命名注释ok么,代码风格和编码技巧ok么?
业务CheckList
- 代码是否实现了预期的业务需求,功能满足么?
- 逻辑是否正确,是否处理了各种异常情况,异常情况都hold住么,异常抛出方式合理么?
- 日志打印是否得当,是否方便 debug 排查问题,好查问题么?
- 接口是否易用,是否支持幂等、事务等,简明稳定么,可以重复调用么?
- 代码是否存在并发问题,是否线程安全,高并发下扛的住么?
- 性能是否有优化空间,比如,SQL、算法是否可以优化,性能还能更好么?
- 是否有安全漏洞,比如输入输出校验是否全面?质量高么,容易出错么?
有了诊断结果,就能对症下药,多轮重构,小步快跑搞定
总结一下
代码重构实际上是设计思想、设计原则、设计模式、编程规范的一个练兵场,通过掌握这些知识,对代码存在的问题进行诊断,依据诊断结果进行重构,才能保证写出高质量有活力的代码!