软件配置管理(四)代码味道与重构
重构的概念及意义
重构是使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。提高其可理解性,降低修改成本。
重构可以改进软件设计、使软件更加容易理解、帮助找到软件缺陷、提高变成速度。
代码味道
指程序中存在的一些不良的编程或设计方案。可以作为重构的指示。
代码味道分类
1.类内味道
1.1 可度量的味道-Measured Smells
1.1.1 过长函数-Long Method
方法太长。
手段:
- 把函数变小:提炼函数
- 函数内有大量参数和临时变量:以查询代替临时变量
- 参数太多:引入参数对象
- 太多临时变量和注释:以函数对象取代函数
- 条件表达式和循环:分解条件表达式
1.1.2 过大类-Large Class
一个类职责过多,拥有过多的实例变量。
手段:
- 太多实例变量或太多代码:提炼类、提炼子类
- 确定客户端如何使用:提炼接口
- 把数据和行为移到一个独立的领域对象,但保留一些重复数据:复制“被监视的数据”。
1.1.3 过长参数列-Long Parameter List
一个方法需要传递太多的参数。
手段:
- 向已有对象发出一条请求就可以取代一个参数:以函数取代参数
- 参数缺乏合理的对象归属:引入参数对象
- 将来自一个对象的参数收集起来:保持对象完整
1.1.4 过多的注释-Comments
在非必要时不要写注释,优先重构代码以使代码具有自解释性。
手段:
- 需要注释来解释一块代码做了什么:提炼函数
- 函数已经提炼出来,但仍需注释:函数改名
- 需要注释来说明系统的需求规格:引入断言
1.2 不必要的复杂性-Unnecessary Complexity
1.2.1 夸夸其谈的未来性-Speculative Generality
当程序中过量的使用设计模式,导致在代码的阅读过程中很难找到主要的逻辑走向。放置过量的钩子或特殊情况来处理一些非必要的事情,可能在代码的编写调试过程中加深跟踪Bug的难度。
手段:
- 某个抽象类没有太大作用:折叠继承体系
- 不必要的委托:将类内联化
- 函数的某些参数未被使用:移除参数
- 函数名称带有多余的抽象含义:函数改名
- 无用函数:内联函数、移除函数
1.3 重复-Duplication
1.3.1 重复代码-Duplicated Code
在多个地方发现相似代码结构,
手段:
- 同一个类含有相同代码:提炼函数
- 两个兄弟类有相同代码:提炼函数、函数上移、塑造模板函数
- 两个不相干类有相同代码:提炼类
1.3.2 异曲同工的类-Alternative Classes with Different Interfaces
不同的类完成相同的任务,但有不同的签名(主要指方法签名)。
手段:
- 两个函数做同一件事,但有不同签名:函数改名
- 将某些行为移入类,直到两者协议相同:搬移函数
- 必须重复而冗赘移入代码才能实现上述重构:提炼超类
1.4条件逻辑-Conditional Logic
1.4.1 Switch惊悚现身-Switch Statements
Switch语句(包括if-else)通常会导致代码重复。
手段:
- 与类型码相关的函数或类:提炼函数、搬移函数、以多态取代条件表达式、以子类取代类型码、以状态模式或策略模式取代类型码。
- 只在单一函数中有一些选择事件:以明确函数取代参数
- 选择条件之一是null:引入null对象。
2.类间味道
2.1 数据-Data
2.1.1 基本类型偏执-Primitive Obsession
基本类型被过度使用。
手段:
- 将原本单独存在的数据值替换成对象:以对象代替数据值
- 如果想替换的是类型码,而类型码不影响行为:以类取代类型码
- 如果有与类型码相关的条件表达式:以子类取代类型码、以状态模式、策略模式取代类型码
- 如果有一组应该总是被放在一起的字段:提炼类
- 如果在参数列中看到基本类型的数据:引入参数对象
- 发现自己正从数组中挑选数据:以对象取代数组
2.1.2 纯稚的数据类-Data Class
类只拥有成员变量和对应的setter和getter方法。
手段:
- 对于public字段:封装字段
- 如果一个容器的字段没有得到恰当的封装:封装集合
- 对于那些不该被其他类修改的字段:移除设值函数
- 找出getter和setter被其他类运用的地点:搬移函数
- 如果无法搬移整个函数:提炼函数
- 将getter和setter隐藏起来:隐藏函数
2.1.3 数据泥团-Data Clumps
一些数据项同时出现在多个地方,如一对内的成员变量、多个方法签名的参数等。
手段:
- 两个类有相同的字段或许多函数签名中的参数相同:提炼类
- 缩短参数列表、简化函数调用:引入参数对象、保持对象完整
2.1.4 令人迷惑的暂时字段-Temporary Field
一个对象中某个变量仅为某些特定场合而设,导致代码难以理解。
手段:
- 一个对象中某个实例对象仅为某种特定情况而定:提炼类
- 在“变量不合法”的情况下创建一个null对象,从而避免写出条件式代码:引入null对象
2.2 继承-Inheritance
2.2.1 被拒绝的遗赠-Refused Bequest
子类继承父类的方法和数据,但仅使用其中一部分。
手段:
- 子类不想或不需要继承超类(先为子类新建一个兄弟类):提炼子类、函数下移、字段下移
- 子类复用了超类的行为,却又不愿意支持超类的接口,拒绝继承超类的实现:以委托代替继承
2.2.2 狎昵关系-Inappropriate Intimacy
类之间的关系变得过于紧密。
手段:
- 类之间的关系过于紧密:搬移函数、搬移字段、将双向关联改为单向关联
- 如果两个类存在共同点:提炼类、隐藏“委托关系”
- 从继承体系中分离子类:以委托取代继承
2.2.3 冗赘类-Lazy Class
如果一个类不值其身价,它就应该消失。
手段:
- 对于几乎没用的组件:将类内联化
- 如果某些子类没有做足够的工作:折叠继承体系
2.3 职责-Responsibility
2.3.1 依恋情结-Feature Envy
一个方法对别的类的兴趣高于它本身所在的类。
手段:
- 函数对某个类的兴趣高过对自身所处类的兴趣:搬移函数、搬移字段
- 函数中只有一部分对其他类更感兴趣:提炼函数、搬移函数
2.3.2 过度耦合的消息链-Message Chains
一个对象请求另一个,后者在请求下一个对象,…这就是消息链【a.getB().getC().getD()】。采取这种方式,意味客户代码将与查找过程中的导航结构紧密耦合,一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。
手段:
- 隐藏“委托关系”
2.3.3 中间人-Middle Man
一个类的接口中,一半的方法都委托给了其他类。
手段:
- 过度运用委托:移除中间人
- 如果“不干实事”的函数只有几个:内联函数
- 如果中间人还有其他行为,需要对原对象的行为进行扩展:以继承取代委托
2.4 协调变化-Accommodating Change
2.4.1 发散式变化-Divergent Change
一个类拥有多个引起它变化的原因。
手段:
- 找出某特定原因而造成的所有变化:提炼类
2.4.2 霰弹式修改-Shotgun Surgery
进行某种修改时,必须对多个不同的类进行对应的小修改。
手段:
- 把所有需要修改的代码放进同一个类中:搬移函数、搬移字段
- 如果眼下没有合适的类可以安置这些代码,就创造一个
- 把一系列相关行为放进同一个类中:将类内联化
2.4.3 平行继承体系-Parallel Inheritance Hierarchies
当为某个类增加子类时,不得不为另一个类也增加一个子类。
手段:
- 让一个继承体系的实例引用另一个继承体系的实例:搬移函数、搬移字段
2.5 库类-Library Classes
2.5.1 不完善的程序库类-Incomplete Library Class
手段:
- 如果只想修改库类的一两个函数:引入外加函数
- 如果想添加大量额外行为:引入本地扩展