重构
重构指在不改变程序原有行为的基础上,对既有代码进行修改,以改进其内部结构。
何时应该重构
添加功能时重构,修复bug时重构,代码评审时重构;
何时不应重构
既有代码太过混乱或不能正常运作,项目已近最后期限。
重构的好处
1. 重构可以改进软件设计
项目结束后,后期的bug修复、需求增加会导致代码逐渐腐败变质。冗余、结构混乱、难以理解、难以维护,难以扩展。如要修改某个错误,可能涉及到要修改的代码点很多。
软件开发中唯一保持不变的就是变化。当软件因为需求变更而开始逐渐退化时,运用软件重构改善我们的结构,使之重新适应软件需求的变化。经常性的重构可以维护代码原有的形态。
- 重构可以帮助理解代码
在理解代码时,尝试去按自己的理解修改,使代码更趋简洁,随之而来的是看到一些以前看不到的设计层面的东西。
重构原则:小步快跑
敏捷软件开发的一个关键活动就是迭代,倡导进化式设计、增量开发。
在重构中也应采取相似的策略:小步快跑、进化式重构。每次修改一点点并测试,以保证不会引入bug。改动过大可能导致结果不可控,出现很多意想不到的bug。
代码坏味道
冗余代码、命名不规范、重复代码、过长函数、臃肿的类、参数过多、过度设计、过多的注释等等。
常用重构技巧(建议)
- 代码风格
在已有代码上进行重构,重构后的代码应尽量与以前的好的代码风格(命名规则)保持一致。原来不好的风格,如变量跟运算符间未添加空格、函数参数之间空格、函数之间未空出一行等等,应进行修改。
- 删除冗余代码
某个函数、类、变量如果不再工作,就应该将其删除。过多的冗余代码将会增加他人理解代码的难度,且使编译后的目标文件增大。局部变量应在使用前定义,没必要在函数入口处一次定义。
- 重命名
模糊不清的方法名会影响代码的可用性。这些模糊不清的名称应该重命名为有意义且与业务有关的名称,来帮助更好地理解代码
对类,接口,方法,变量、参数等重命名,以使得更易理解。名称应该能比较清晰的说明该函数、变量、类的职责。好的名称可以达到自注释的目的。类似于generalCall、setPriceAndWeight就不是很好的名字。
- 常量替换魔法数字
对于有意义的并且到处被使用的魔法数字,应该使用常量替代。这能大大增强代码可读性和可理解性。
- 函数参数过多
如果一个函数或方法的参数过多就会出现如果更改了其中一个参数, 就得在多个调用点进行更改。可以将多个参数封装成一个结构体或类。
- 分解臃肿函数、类承担的多个职责
设计模式有一条原则叫做单一职责原则。也就是说函数、类、接口承担的职责应尽量单一。过多的职责会导致函数体、类方法过于庞大(几百上千行代码)、类职责过多影响代码阅读这是其一。其二可能存在重复代码,重复代码很多是通过代码拷贝实现,可能会导致错误扩散。其三多个职责就会存在多个变化点,在修改其中一 个职责时可能会影响其他代码,多个职责耦合性大,可能会引入错误。
在实际使用中,应尽量做到函数、接口、方法职责单一。类尽量做到职责单一。
- 过大的类拆分成多个类
将一个类承担的多个职责拆分成多个类,以使每个类职责相对单一。
方法:原有类中的方法和属性移动到新类。有时候一些类过于臃肿是因为它包含应该在其他类定义的方法。这些方法也应该被迁移到合适的类中。如负责Ui交互的功能与业务逻辑分开、数据库访问与业务逻辑拆分成不同的类,可以对数据库访问添加间接层,以兼容不同数据库的改变对业务逻辑的影响。
- 过长的方法
将过长的函数应分解成多个命名良好的小函数。更容易理解且具有更好的复用性。很多程序员担心带来性能损耗,拆分后的多个小函数调用的性能消耗微乎其微。与其带来的好处相比可以忽略。如果确实导致性能损耗可以再通过重构改善性能。
- 消除重复代码
重复代码会使目标文件体积增大。很多是通过代码拷贝实现,可能会导致错误扩散。
- 提取成函数。
提取重复代码定义成独立函数,函数粒度小复用的机会就大。一旦需要修改仅需修改一处 ,职责单一容易理解。
- 提取方法
若重复代码位于同一继承体系中,可以提取成基类的方法。
若并不是完全重复,存在微小的差异,可以使用模板方法模式。
- 继承泛滥
通过继承和组合都可以使一个类获得另一个类的功能。但使用继承时子类与父类是强依赖关系。在使用父类指针或引用的地方都可以使用子类替代父类。也就是说只有当两个类之间确实存在is-a关系时才能使用继承。强依赖关系使得子类父类耦合性很强。而组合相比较来说依赖减弱,当满足has-a关系时就可以通过组合来实现。
- 适当降低圈复杂度
圈复杂度用来衡量一个模块判定结构的复杂程度,也可理解为覆盖所有的可能情况最少使用的测试用例数。复杂度高的代码判断逻辑复杂,可能会引入bug,且可读性很差。
圈复杂度主要与分支语句(if、else、,switch 等)的个数有关。当一段代码中含有较多的分支语句,其逻辑复杂程度就会增加。有目的的降低核心类、核心方法的复杂度,可以降低软件的风险,增加软件的可扩展性。
- 简化条件表达式
- 合并条件表达式
若一系列条件判断,得到相同结果,可以将这些测试合并为一个独立函数。
if(nHour <0 || nHour > 60)
return false;
if(nMinute < 0 || nMinute > 60)
return false;
if(nSec <0 || nSec > 60)
return false;
//其他代码
合并后:
if(false == isTimeValid(nHour, nMinute, nSec))
{
return false;
}
//其他代码
bool isTimeValid(int nHour, int nMinute, int nSec)
{
if(nHour < 0 || nHour > 60 || nMinute < 0 || nMinute > 60 || nSec <0 || nSec > 60)
{
return false;
}
return true;
}
- 避免多层嵌套条件表达式
条件表达式通常有两种形式: 所有分支都属于正常语句以及只有一个分支属于正常语句,其他都是非正常情况。 如果某个条件属于异常条件且不太常见,则应该单独检查该条件。嵌套导致代码可读性差,应尽量避免。
if(NULL != pBuf)
{
if(false != func1())
{
if(false != Func2())
{
Func3();
}
}
}
修改后:
if(NULL == pBuf)
return false;
if(false == func1())
return false;
if(false == func2())
return false;
func3();
没有了嵌套,结构更清晰易懂。
- 使用多态取代条件表达式(if else switch case)
若在某个条件表达式中存在根据类型的不同具有不同的行为。可以考虑将原始函数声明为抽象函数,将条件表达式的每个分支放进一个子类重写的方法中。使用多态不必编写某些条件表达式并且若你想添加一种新类型,只需创建一个新的子类并重写该方法。这些更改对类的用户是透明的,上层不需要做任何更改。
- 避免过度超前设计
过度设计是指代码的灵活性和复杂性超出了所需。代码应该满足当前的需求,并留有可扩展的余地。对于未来的变化,既不要考虑的太多,也不能一点都不考虑。刚开始的时候可能需求并不明确、或者我们对需求的理解还不甚明确,不能照顾到所有的变化,因为变化可能来自很多个方向。所有方向都考虑到将、过度的超前设计会导致我们的代码很复杂。在需求不甚明确时,很多超前的设计都是多余的。可以从中选择几个最可能发生变化的方向,使用设计模式来兼容这些可能的变化。后期随着我们对需求理解的不断加深,或是需求在某个方向上确实发生了变化,这时我们再回过头来通过重构来改进我们的设计,使其兼容此变化。
有一个比喻,就是说允许被来自同一个方向的同一把手枪的子弹击中一次。当你被击中之后,你应该意识到这个方向很危险,应该立即采取动作。如果反应迟钝,再次被击中就是不可原谅的。
推荐书籍:
《重构-改善既有代码的设计》