规范与重构
# 规范与重构
# 了解重构
# 重构的目的:为什么要重构(why)?
重构的含义
重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低
重构的意义
首先,重构是时刻保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步。
其次,优秀的代码或架构不是一开始就能完全设计好的,就像优秀的公司和产品也都是迭代出来的。
最后,重构是避免过度设计的有效手段。
重构对一个工程师本身技术的成长也有重要的意义。
# 重构的对象:到底重构什么(what)?
根据重构的规模,我们可以笼统地分为大规模高层次重构(以下简称为“大型重构”)和小规模低层次的重构(以下简称为“小型重构”)。
# 重构的时机:什么时候重构(when)?
当开发时,有时间就可以思考下,自己以前写的东西是否可以优化,其实就是边开发边重构。
开发者应该拥有持续重构意识。
# 重构的方法:又该如何重构(how)?
大规模高层次的重构难度比较大,需要组织、有计划地进行,分阶段地小步快跑,时刻让代码处于一个可运行的状态。而小规模低层次的重构,因为影响范围小,改动耗时短,所以,只要你愿意并且有时间,随时随地都可以去做。
# 为了保证重构不出错,使用单元测试技术手段
# 1. 什么是单元测试?
单元测试是代码层面的测试,由研发自己来编写,用于测试“自己”编写的代码的逻辑的正确性。单元测试顾名思义是测试一个“单元”,有别于集成测试,这个“单元”一般是类或函数,而不是模块或者系统。
# 2. 为什么要写单元测试?
写单元测试的过程本身就是代码 Code Review 和重构的过程,能有效地发现代码中的 bug 和代码设计上的问题。除此之外,单元测试还是对集成测试的有力补充,还能帮助我们快速熟悉代码,是 TDD 可落地执行的改进方案。
# 3. 如何编写单元测试?
写单元测试就是针对代码设计各种测试用例,以覆盖各种输入、异常、边界情况,并将其翻译成代码。我们可以利用一些测试框架来简化单元测试的编写。除此之外,对于单元测试,我们需要建立以下正确的认知:
编写单元测试尽管繁琐,但并不是太耗时;
我们可以稍微放低对单元测试代码质量的要求;
覆盖率作为衡量单元测试质量的唯一标准是不合理的;
单元测试不要依赖被测代码的具体实现逻辑;
单元测试框架无法测试,多半是因为代码的可测试性不好。
# 4. 单元测试为何难落地执行?
一方面,写单元测试本身比较繁琐,技术挑战不大,很多程序员不愿意去写;另一方面,国内研发比较偏向“快、糙、猛”,容易因为开发进度紧,导致单元测试的执行虎头蛇尾。最后,关键问题还是团队没有建立对单元测试正确的认识,觉得可有可无,单靠督促很难执行得很好。
课堂讨论
你参与的项目有没有写单元测试?单元测试是否足够完备?贯彻执行写单元测试的过程中,遇到过哪些问题?又是如何解决的?
在面试中,我经常会让候选人写完代码之后,列举几个测试用例,以此来考察候选人考虑问题是否全面,特别是针对一些边界条件的处理。所以,今天的另一个课堂讨论话题就是:写一个二分查找的变体算法,查找递增数组中第一个大于等于某个给定值的元素,并且为你的代码设计完备的单元测试用例。
个人回答:
- 没有怎么写过单元测试,一般只是把一个业务当做测试代码来测,这样只能保证代码主业务线是通的,但是测试不到边界问题,而且每个函数测试不到
- 代码 (opens new window)
# 如何通过技术手段解耦?
# 1.“解耦”为何如此重要?
过于复杂的代码往往在可读性、可维护性上都不友好。解耦保证代码松耦合、高内聚,是控制代码复杂度的有效手段。代码高内聚、松耦合,也就是意味着,代码结构清晰、分层模块化合理、依赖关系简单、模块或类之间的耦合小,那代码整体的质量就不会差。
# 2. 代码是否需要“解耦”?
间接的衡量标准有很多,比如,
看修改代码是否牵一发而动全身。
直接的衡量标准是把模块与模块、类与类之间的依赖关系画出来,根据依赖关系图的复杂性来判断是否需要解耦重构。
# 3. 如何给代码“解耦”?
给代码解耦的方法有:封装与抽象、中间层、模块化,以及一些其他的设计思想与原则,比如:单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特法则等。当然,还有一些设计模式,比如观察者模式。
# 编程规范
# 1. 关于命名
命名的关键是能准确达意。对于不同作用域的命名,我们可以适当地选择不同的长度。作用域小的变量(比如临时变量),可以适当地选择短一些的命名方式。除此之外,命名中也可以使用一些耳熟能详的缩写。
我们可以借助类的信息来简化属性、函数的命名,利用函数的信息来简化函数参数的命名。
命名要可读、可搜索。不要使用生僻的、不好读的英文单词来命名。除此之外,命名要符合项目的统一规范,不要用些反直觉的命名。
接口有两种命名方式:一种是在接口中带前缀“I”;另一种是在接口的实现类中带后缀“Impl”。对于抽象类的命名,也有两种方式,一种是带上前缀“Abstract”,一种是不带前缀。这两种命名方式都可以,关键是要在项目中统一。
# 2. 关于注释
注释的目的就是让代码更容易看懂。只要符合这个要求的内容,你就可以将它写到注释里。总结一下,注释的内容主要包含这样三个方面:做什么、为什么、怎么做。对于一些复杂的类和接口,我们可能还需要写明“如何用”。
注释本身有一定的维护成本,所以并非越多越好。类和函数一定要写注释,而且要写得尽可能全面、详细,而函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码可读性。
# 3. 代码风格
1. 函数、类多大才合适?
函数的代码行数不要超过一屏幕的大小,比如 50 行。类的大小限制比较难确定。
2. 一行代码多长最合适?
最好不要超过 IDE 显示的宽度。当然,限制也不能太小,太小会导致很多稍微长点的语句被折成两行,也会影响到代码的整洁,不利于阅读。
3. 善用空行分割单元块 对于比较长的函数,为了让逻辑更加清晰,可以使用空行来分割各个代码块。在类内部,成员变量与函数之间、静态成员变量与普通成员变量之间、函数之间,甚至成员变量之间,都可以通过添加空行的方式,让不同模块的代码之间的界限更加明确。
4. 四格缩进还是两格缩进?
我个人比较推荐使用两格缩进,这样可以节省空间,特别是在代码嵌套层次比较深的情况下。除此之外,值得强调的是,不管是用两格缩进还是四格缩进,一定不要用 tab 键缩进。
5. 大括号是否要另起一行?
我个人还是比较推荐将大括号放到跟上一条语句同一行的风格,这样可以节省代码行数。但是,将大括号另起一行,也有它的优势,那就是,左右括号可以垂直对齐,哪些代码属于哪一个代码块,更加一目了然。
6. 类中成员的排列顺序
在 Google Java 编程规范中,依赖类按照字母序从小到大排列。类中先写成员变量后写函数。成员变量之间或函数之间,先写静态成员变量或函数,后写普通变量或函数,并且按照作用域大小依次排列。 今天讲到所有的代码风格都没有对错和优劣之分,只要能在团队、项目中统一即可,不过,最好能跟业内推荐的风格、开源项目的代码风格相一致。
# 关于编码技巧
- 将复杂的逻辑提炼拆分成函数和类。
- 通过拆分成多个函数或将参数封装为对象的方式,来处理参数过多的情况。
- 函数中不要使用参数来做代码执行逻辑的控制。
- 函数设计要职责单一。
- 移除过深的嵌套层次,方法包括:去掉多余的 if 或 else 语句,使用 continue、break、return 关键字提前退出嵌套,调整执行顺序来减少嵌套,将部分嵌套逻辑抽象成函数。
- 用字面常量取代魔法数。
- 用解释性变量来解释复杂表达式,以此提高代码可读性。