重构改善既有代码
重构改善既有代码
- 第一次做某件事情的时候尽管去做,第二次做类似的事会产生反感,第三次再做类似的事,你就应该重构。
- 小型函数优美动人
- 一个类最好是常量类,任何的改变都是调用该类本身的接口实现。
0 坏代码的味道
1、重复代码
- duplicated code
- 同一类中的两个函数含有相同的表达式,提取到方法
- 互为兄弟的子类含有相同表达式,将两个子类的相同代码提取方法推入超类
- 如果有相似代码,通过提炼方法将相似和差异部分分割开,并使用疏凿模板方法,并将模板方法上移到超类中。
- 如果两个毫不相关的类出现重复代码,将重复代码提炼到一个提炼类中,两个类都使用这个提炼类。
2、过长函数
- long method
- 间接层所能带来的全部利益——解释能力、共享能力、选择能力
- 小函数的价值是巨大的
- 每当感觉需要注释来说明什么的时候,就需要把说明的东西写进一个独立函数中,并以其用途命名。
- 函数内有大量的临时变量和参数。需要运用提炼方法,可以将临时变量作为参数传入,也可以使用以查询替代临时变量,当方法参数特别多的时候可以提炼参数类,传递参数类实体。如果这么做还有很多的参数,那么就应该用方法对象来取代方法了。
- 选择提炼哪一段代码
- 寻找注释,有注释的地方都在提醒你需要提炼方法了,注释名称就是很好的方法名
- 条件表达式和循环也是型号,可以用 分解条件表达式,循环应该将循环中的代码提炼到独立函数中。
3、过大的类
- large class
- 如果单个类做太多是事情,往往会导致出现太多的实例变量,一旦如此,重复代码就接踵而至了。
- 可以使用提炼类将几个变量和方法提炼出来,如果数个变量存在着相同的前缀或字尾,就以为着有机会可以把它们提炼到某个组件中。如果这个组件适合一个子类,还可以使用提炼子类。
- 如果一个拥有太多代码,可以先确定客户端如何使用它们,然后运用提炼接口,为每一种使用方法提炼出一个接口,这可以看清楚如何分解这个类。
- 如果超大类是一个gui类,可以把数据和行为移到一个独立的领域对象去,可能需要两边保留一些重复代码,并保持两边同步。
4、过长的参数列
- long parameter list
- 如果向已有的对象发出一条请求就可以取代一个参数,那么就可以使用用方法取代参数方法。
- 还可以使用保持整个对象,传递整个对象,
- 提炼参数对象
- 造成函数关联需要慎重考虑
5、发散式变化
- divergent chane
- 软件再怎么说就应该是软的,一旦需要修改,希望能够跳到系统的某一点,只在该处做修改。如果不能的化就有一种刺鼻味道了。
- 某个类经常因为不同原因在不同不同方向上发生变化发散式变化就出现了,
- 一旦出现这种发散式变化那么就需要将对象分解成多个对象或者会更好,当出现多个类后还可以提炼超类等。
6、霰弹式修改
- shotgun surgery
- 正对某一项变化需要在许多不同类种做出需要小修改,所面临的味道就是霰弹式修改,
- 这种情况应该使用移动方法和移动字段,把所有修改的代码放进同一个类,如果没有现存的类可以按值这些代码就创造一个,使用内联类可以将一系列相关行为放进同一个类。
- 这也可能造成少量的发散式变化,
7、依恋情结
- feature envy
- 对象技术的全部要点在于:这是一种将数据和对数据的操作行为包装在一起的技术,有一中经典的气味是:函数对某个类的兴趣高于对自己所处类的兴趣。
- 使用移动方法把某些方法移动带它该去的地方,有的时候还需要提炼方法
- 如果某个函数需要需要几个类的功能,判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起,可以先将函数分解成多个较小函数分别置于不同地点。
- 将总是一起变化的东西放在一块,数据和引用这些数据的行为总是一起变化的。
- 策略和访问者模式可以轻松修改函数行为,付出了多一层的代价
8、数据泥团
- data clumps
- 数据项会成群结队出现。
- 如果删除总舵数据中的一项,其他数据有没有失去意义,如果它们不再有意义,就是一个明确的信号,应该产生一个新对象。
9、基本类型偏执
- primitive obsession
- 结构类型允许你将数据组织成有意义的形式,对象的极大价值在于打破了横亘于基本数据和较大类之间的界限。
- 积极的使用使用对象替换数据值,用类替换类型码,用状态/策略模式替代类型码
10、swithc惊悚现身
- switch statements
- 面向对象程序的最明显特征就是少用switch,使用switch的问题在于重复,在修改上,如果switch散布于不同地点,就要添加新的case子句
- 如果看到switch语句的时候需要考虑用多态来替换它,问题在于多态出现在哪儿
- 使用提炼函数将switch提炼到独立函数中,再用移动方法将它搬移到需要多态性的类中,用子类替代类型码或者使用state/strategy替代类型码,完成之后再用使用多态替代条件。
11、平行继承体系
- parallel inheritance hierarchies
- 如果为某个类增加一个子类的时候必须要为另一类相应增加一个子类。
- 如果某个继承体系的类名称前缀和两一个继承体系的类的名称前缀完全相同
- 让一个继承体系的实例引用另一个继承体系的实例,再使用移动方法和字段,就可以将引用端的继承体系消除。
12、冗赘类
- lazy class
- 创建的每个类都有人去理解它维护它,如果一个类不值得其身价就应该消失。
13、夸夸其谈的未来性
- speculative generality
- 总有一天需要做这件事,企图以各式各样的勾子和特殊情况来处理一些非必要事情会造成程序难以理解。不需要
14、令人迷惑的暂时字段
- temporary field
- 某个实例变量仅为某种特定情况而设置。
- 使用提炼类给这些孤儿创造一个家,然后把所有和这个变量相关的代码都放进这个新家,还可以使用空对象方法创建一个空对象。
15、过度耦合的消息链
- message chains
- 一个对象请求一个对象,然后后者请求另一个对象,等等
- 使用隐藏委托。
16、中间人
- middle man
- 对象的基本特征之一就是封装,对外部世界隐藏内部细节,封装往往伴随着委托,但有可能过度使用委托,移除中间人
17、狎昵关系
- inappropriate intimacy
- 两个类过于亲密,移动方法和字段让他们划清界限。如果划清不了就使用提炼类让他们融为一体吧
18、异曲同工类
- alternative classes with different interfaces
- 重命名方法,提炼子类
19、不完美的库类
- incomplete library class
- 给库类加入新的方法,外部方法和本地扩展。
20、纯稚的数据类
- data class
- 不会说话的数据容器一定被其他类过分的操控着,运用封装字段封装,移动设置方法,移动方法,提炼方法。
21、被拒绝的遗赠
- refused bequest
- 子类不愿全部继承,为这个子类创建一个兄弟类,在运用下移方法和字段把用不到的函数下推给那个兄弟,这样一来,超类就只持有所有子类共享的东西。
- 用委托替换继承
22、过多注释
- comments
- 提炼方法。
1 重新组织函数
对函数的重构方法
1、提炼函数
- extractmethod
- 动机
- 每个函数的颗粒度都比较小,高层函数读起来就像是注释
- 颗粒度比较小覆写也比较容易
- 什么时候需要提炼函数
- 当函数体的语义与函数名称偏离的时候就需要提取
- 怎么提取
- 将代码提取出来用函数的意图来命名(做什么)
- 如果该代码段中有读取或改变临时变量
- 该临时变量在原函数中有没有使用,
- 优先考虑用查询取代临时变量
- 没有直接将临时变量的声明移植到函数体中
- 在函数体之前使用,作为参数传入
- 在函数体之后使用,作为函数返回值返回
- 之前之后都使用,作为参数传入,在作为返回值返回
- 该临时变量在原函数中有没有使用,
- 如果临时变量非常多,
- 需要考虑这个函数体是否真的属于这个类
- 以查询替代临时变量
2、内联函数
- inline method
- 什么时候需要内联
- 当函数的本体和名称同样清楚易懂的时候
- 当有一大群组织不太合理的函数,想重构的时候,将一大群函数内联然后重新提取
- 有太多的间接层,所有函数似乎都是对另一个函数的简单委托
- 怎么内联
- 检查函数,确定它不具有多态。
- 找出该函数的所有引用点,用函数体替换(最好用文本查找的方式找)
3、内联临时变量
- inline temp
- 动机
- 什么时候做
- 有一个临时变量,只被简单表达式赋值一次,而它妨碍其他重构手法
- 怎么做
4、以查询取代临时变量*
- replace temp with query
- 动机
- 临时变量是暂时的,如果这个临时变量需要被使用多次就考虑需要用查询取代,这边的查询可以直接使用.net中的属性。
- 临时变量会驱使函数变长,如果变成查询,类中的其他成员也可以访问。
- 什么时候需要查询取代
- 用一个临时变量保存其某一表达式的运算结果,需要一个查询函数取代临时变量
- 怎么取代
- 需要分解临时变量(临时变量被赋值超过一次),以查询取代临时变量,然后再替换临时变量
- 首先应该将查询设置为私有的,当日后需要的时候再开放保护。
- 不用考虑细微的性能问题,因为首先需要良好的架构才能使得程序正常运行。然后再考虑性能问题。
5、引入解释性变量
- introduce explaining variable
- 在引入解释性变量之后,可以使用导出方法或者用查询取代临时变量将临时变量替换掉。
- 动机
- 使得复杂表达式可以阅读和管理
- 什么时候需要引入
- 有一个复杂的表达式
- 怎么引入
- 讲一个复杂表达式(或一部分)的结果放进一个临时变量,以此变量名称来解释表达式的用途
- 与提炼函数的区别
- 再提炼函数需要花费更大的工作量的时候
6、分解临时变量
- split temporary variable
- 动机
- 如果一个临时变量承担太多的职责,会使得阅读者糊涂
- 什么时候分解
- 程序中有某个临时变量被赋值超过一次,它既不是循环变量也不是收集计算结果。
- 怎么分解
- 修改临时变量的名称并声明为常量
7、移除对参数的赋值*
- remove assignments to parameters
- 这边的是针对函数参数体成员
- 对参数的赋值的想法是比较危险的,一旦为参数进行赋值如果混淆值类型和引用类型非常容易产生不易察觉的错误。
- 动机
- 因为面向对象的方式,所以数值类型的改变并不会改变原来传入的值,但是引用类型就会变化
- 导致混用按值传递和按引用传递
- 什么时候移除
- 代码对函数的一个参数进行赋值时
- 怎么移除
- 通过建立一个临时变量,对临时变量进行修改,然后返回临时变量。
- 如果需要返回一大堆函数,可以将返回的一大堆函数变成一个单一的对象,或者为每个返回值设置一个独立函数。
- 还可以在函数的每个参数中增加一个const,这个方法只是在函数体较长的时候才可以使用。
8、以函数对象取代函数
- replace method with method object
- 动机
- 小型函数优美动人
- 什么时候取代
- 有一个大型函数,对其中的局部变量的使用无法采用提炼方法的手段
- 怎么提取
- 建立一个新类,将所有的局部变量变成字段,然后将原函数体中的逻辑变成方法。
9、替换算法
- substitute algorithm
- 动机
- 发现一个算法的效率更高的时候
- 什么时候替换
- 想法把某个算法换成另一个更为清晰的算法
2 在对象之间搬移特性
在面向对象的设计中,决定把责任放在哪里。
先使用移动字段,在移动方法
1、搬移函数
- move method
- 动机
- 一个类与另一个类高度耦合,就会搬移函数,通过这种手段,可以使得类更加简单。
- 什么时候搬移
- 有个函数与其所属类之外的另一个类有更多的交流。
- 当不能肯定是否需要移动一个函数,需要继续观察其他函数,先移动其它函数就会使决定变得容易一些。
- 怎么搬移
- 检查所有字段,属性和函数,考虑是否应该被搬移
- 在该函数最常用引用中建立一个有类似行为的新函数
- 将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。
- 有多个函数使用这个需要搬移的特性,应考虑使用该特性的所有函数被一起搬移。
- 检查所有子类和超类,看看是否有该函数其他声明
- 如果目标函数使用了源类中的特性,可以将源对象的引用当作参数(多个参数或则存在方法需要调用),传给新建立的目标函数。
- 如果目标函数需要太多源类特性,就得进一步重构,会将目标函数分解并将其中一部分移回源类。
2、搬移字段
- move field
- 动机
- 随着项目类的增加和扩充,有一些字段放在原来的类中已经不太合适
- 什么时候搬移
- 某个字段在另一个类中被更多的用到
- 怎么搬移
- 修改源字段的所有用户,令它们改用新字段
- 决定如何在源对象中引用目标对象,方法,新建字段引用
- 新类中自我封装setvalue, getvalue。
3、提炼类*?
- extract class
- 动机
- 将复合类的职责提炼出新的类
- 或者需要将类的子类化,分解原来的类
- 什么时候提炼
- 某个类做了应该由两个类做的事
- 怎么提炼
- 建立一个新类,将相关的字段和函数从旧类搬移到新类
- 有可能需要一个双向连接, 但是在真正需要它之前,不要建立从新类往旧类的连接,如果建立起双向连接,检查是否可以将它改为单向连接。
4、将类内联化
- inline class
- 动机
- 一个类不再承担足够责任,不再由单独存在的理由。
- 什么时候内联
- 某个类没有做太多的事情
- 怎么内联
- 将这个类是多有特性搬移到另一个类中,然后移除原类
- 修改所有源类引用点,改而引用目标类
5、隐藏“委托关系”
-
hide delegate
局限性是每当客户要使用受托类的新特性时,就必须在服务段添加一个简单委托函数,受托类的特性越来越多,这一过程会越来越痛苦。
简单委托关系
-
动机
- 封装意味着每个对象都应该尽可能少的了解系统的其他部分,
- 如果客户调用对象字段得到另一个对象,然后再调用后者的函数,那么客户就必须知道这一层关系。将委托关系隐藏起来不会波及客户。
-
什么时候隐藏
- 客户通过一个委托类来调用另一个对象
-
怎么隐藏
- 在服务类上建立客户所需的所有函数,用以隐藏委托关系
- manager=john.getdepartment().getmanager();隐藏=>manager=john.getmanager();隐藏了调用关系。
6、移除中间人
- remove middle man
- 与隐藏委托关系相反
- 动机
- 针对隐藏委托的局限性,当委托的方法越来越多时,服务类就完全变成一个中间人,此时应该让客户直接调用受托类。
- 什么时候移除
- 某个类做了过多的简单委托动作
- 怎么移除
- 让客户直接调用受托类
7、引入外加函数
- introduce foreign method
- 动机
- 发现一个好用的工具类不能修改工具类,添加方法
- 但外加函数终归是权益之计,
- 什么时候需要引入外加函数
- 需要为提供服务的类增加一个函数,但无法修改这个类。
- 怎么引入
- 在客户类中建立一个函数,并以第一参数形式传入一个服务类实例
8、引入本地扩展
- introduce local extension
- 动机
- 在不能修改的类中添加方法,方法的数量超过2个的时候外加函数难以控制,需要将函数组织到一起,通过两种标准对象技术:子类化和包装,子类化和包装叫做本地扩展。
- 在子类化和包装中优先选择子类,
- 使用包装会造成a=b,b不等于a的逻辑,子类等于包装类,包装类不等于子类
- 什么时候引入
- 需要为服务类提供一些额外函数,但无法修改类。
- 怎么引入
- 建立一个新类,使它包含这些额外函数,让这个扩展品成为源类的子类或包装类。
- 子类化方案,转型构造函数应该调用适当的超类构造函数
- 包装类方案,转型构造函数应该传入参数以实例变量的形式保存起来,用作接受委托的原对象。
3 重新组织数据
对于这个类的任何修改都应该通过该类的方法。类拥有一些数据却无所觉,拥有一些依赖无所觉是非常危险的。所以才要封装字段,封装集合,监视数据,用对象替代数组,用对象替代集合,关联改动。
1、自封装字段
- self encapsulate
- 动机
- 直接访问变量的好处:子类可以通过覆写一个函数而改变获取数据的途径,它还支持更灵活的数据管理方式,如延迟初始化等,
- 直接访问变量的好处:代码比较容易阅读,
- 优先选择直接访问的方式,直到这种访问方式带来麻烦位置。
- 什么时候需要自封装字段
- 直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙。
- 怎么自封装
- 为这个字段建立取值/设值函数,并且只以这些函数来访问字段。
2、以对象取代数据值
- replace data value with object
- 动机
- 简单数据不再简单,
- 注意:原来的数据值是值对象,改成对象可能变成引用类型,这样面临的问题是多个实例就不是同一个对象。需要用将引用对象改成值对象方法,
- 什么时候需要对象取代
- 有一个数据项,需要与其他数据和行为一起使用才有意义。
- 怎么对象取代
- 为替换值新建一个新类,其中声明final字段,修改原字段的引用,都修改为对象。
3、将值对象改成引用对象
- change value to reference
- 对于值类型来说,equals和==的功能是相等的都是比较变量的值、
- 对于引用类型来说,==是b比较两个引用是否相等,equals是比较的引用类型的内容是否相等,而使用equals是需要重写的,不然就是调用object中的equals
- 动机
- 值对象一般是基本数据类型,并不在意是否有副本的存在,
- 引用对象是否相等,直接使用==操作符
- 什么时候改引用
- 一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象
- 类的每个实例中的字段都是独立,就是值类型,每个实例都对应一个字段对象。
- 引用类型多个实例可以共用一个字段对象。不是所有
- 怎么改
- 创建简单工厂和注册表,工厂负责生产字段对象,注册表负责保存所有的字段对象
- 类实例通过工厂请求字段实例,工厂通过访问注册表返回字段实例引用。
- 例子
-
- 目前为止customer对象还是值对象,即使多个订单属于同一客户但每个order对象还是拥有自己的customer对象。
- 使用工厂方法替代构造函数
- 此时值对象才变成引用对象,多个实例间都共享同一个引用对象
4、将引用对象改成值对象
- change reference to value
- 这边引用对象改成值对象并不是说需要把引用类型改成基本类型,而是即使引用类型是不同副本,那么相同内容的引用内容也是相等(重写equals())
- 动机
- 如果引用对象开始变得难以使用,或许就应该将它改成值对象。
- 引用对象必须被某种方式控制,而且必须向其控制者请求适当的引用对象,会造成区域之间错综复杂的关联。
- 值对象应该是不可变的(无论何时,调用同一个对象的同一个查询函数都应该得到相同的结果),如果需要改变就需要重新创建一个所属类的实例,而不是在现有对象上修改。
- 什么时候更改
- 有一个引用对象,很小且不可变,而且不易管理。
- 怎么更改
- 检查重构目标是否为不可变对象,建立equals和hashcode方法
- new currency("usd").equals(new currency("usd"));返回false。重写equal和hashcode使其返回true,这样对象就是值对象,不可变。
5、以对象取代数组
- replace array with object
- 动机
- 数组是常见的组织数据的结构,只用于以某种顺序容纳一组相似对象。
- 什么时候需要取代
- 有一个数组,其中的元素各自代表不同的东西
- 怎么取代
- 将数组的每个不同意思都抽象称字段
6、复制被监视的数据
- duplicate observed data
- 动机
- 一个分层良好的系统,用户界面和处理业务逻辑的代码分开
- mvc模式
- 什么时候需要复制
- 有一些领域数据置身于gui控件中,而邻域函数需要访问这些数据
- 怎么复制
- 将该数据复制到一个领域对象中,建立一个observer模式,用以同步领域对象和gui对象内的重复数据
7、将单向关联改成双向关联
-
change unidirectional association to bidirectional
- 有点像观察者模式,控制者是订阅端,被控制者是主题,主题存在辅助函数,用于修改反向指针,订阅端调用辅助函数来修改反向指针。
-
动机
- 随着项目时间的推移需要双向关联
-
什么时候改动
- 两个类都需要使用对方特性,但其间中有一条单向连接
-
怎么实现
添加一个反向指针,并使修改函数能够同时更新两条连接。
在被引用的类中增加一个字段,保存反向指针。
-
控制端和被控制端
- 一对多的关系,可以使用单一引用的一方(就是多的那一方)承担控制者的角色。
- 对象是组成另一对象的部件,该部件负责控制关联关系。
- 如果两者都是引用对象,多对多,那么无所谓。
在被控端建立一个辅助函数,负责修改反向指针
如果既有的修改函数在控制端,让它负责控制修改反向指针
如果既有的修改函数在被控端,就在控制端建立一个控制函数,并让既有的修改函数调用这个新建的控制函数,来控制修改反向指针。
8、将双向关联改为单向关联
- change bidirectional association to unidirectional
- 动机
- 双向关联必须要符出代价,维护双向关联,确保对象被正确创建和删除而增加的复杂度。
- 双向关联还会造成僵尸对象,某个对象已经死亡却保留在系统中,因为它的引用还没有完全清楚。
- 双向关联也会迫使两个类之间有了依赖,对其中任一个类的修改,都可能引发另一个类的变化。
- 什么时候需要
- 两个类之间有双向关联,但其中一个类不再需要另一个的特性
- 怎么修改
- 去除不必要的关联
- 将私有字段去掉,需要依赖的函数,将依赖类作为参数传入,然后调用。
- 创建一个静态字典保存所有的依赖类,通过取值函数来获得字段遍历对比依赖的引用是否相同来获取依赖类。
9、以字面常量取代魔法数
- replace magic number with symbolic constant
- 动机
- 什么时候取代
- 有一个字面数值,并带有特别含义
- 怎么取代
- 创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。
10、封装字段
- encapsulate field
- 动机
- 数据声明为public被看做一种不好的做法,会降低模块化程度。
- 拥有该数据对象却毫无察觉,不是一件好事
- 什么时候封装
- 类中存在一个public字段
- 怎么封装
- 将原字段声明为private,并提供相应的访问函数
11、封装集合
- encapsulate collection
- 除非通过封装的集合类,不然没有任何实例能够修改这个集合。
- 动机
- 在一个类中使用集合并将集合给取值函数,但类不应该返回集合自身,因为这回让用户得以修改集合内容而对集合的使用者一无所知。
- 不应该为集合提供一个设值函数,但应该为集合添加/移除元素的函数,这样集合的拥有者就可以控制集合元素的添加和移除。
- 什么时候封装
- 有一个函数返回一个集合
- 怎么封装
- 让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数
12、以数据类取代记录
- replace record with data class
- 动机
- 从数据库读取的记录,需要一个接口类,用来处理这些外来数据。
- 什么时候做
- 需要面对传统编程环境中的记录结构
- 怎么做
- 为该记录创建一个哑数据对象。
- 新建一个类,对于记录汇总的每一项数据,在新建的类中建立一个对应的private字段,并提供相应的取值和设值函数。
13、以类取代类型码
- replace type code with class
- 原来的类型码可能是int类型,建立一个类型码的类,所有的int转换成类型码的类,其实有点像创建一个枚举类型,然后用枚举类型取代int。
- 动机
- 类型码或枚举值很常见,但终究只是一个数值,如果是一个类就会进行类型检验,还可以为这个类提供工厂函数,保证只有合法的实例才会被创建出来。
- 如果有switch必须使用类型码,但任何switch都应该使用多态取代条件去掉。为了进行这样的重构还需要使用子类取代类型码,用状态或策略替换类型码。
- 什么时候做
- 类之中有一个数值类型码,但它并不影响类的行为
- 怎么做
- 以一个新的类替换该数值类型码
- 用以记录类型码的字段,其类型应该和类型码相同,还应该有对应的取值函数,还应该用一组静态变量保存允许被创建的实例,并以一个静态函数根据原本的类型码返回合适的实例。
14、以子类取代类型码
- replace type code with subclasses
- 动机
- 什么时候做
- 有一个不可变的类型码,它会影响类的行为
- 如果类型码会影响宿主类的行为,最好的做好就是用多态来处理变化行为。就是switch和if else结构。
- 类型码值在对象船舰之后发生变化,类型码宿主类已经拥有子类,这两种情况下就需要使用状态/策略设计模式
- 怎么做
- 以子类取代这个类型码
15、以state/strategy取代类型码
- replace type code with state/strategy
- 每个状态有特定的数据和动作。
- 动机
- 什么时候做
- 有一个类型码,它会影响类的行为,但无法通过继承手法消除它
- 怎么做
16、以字段取代子类
- replace subclass with fields
- 动机
- 什么时候做
- 各个子类的唯一差别只在返回常量数据的函数身上
- 直接用该字段的不同值表示子类就可以了。
- 怎么做
- 修改这些函数,使它们返回超类中某个(新增字段,然后销毁子类)
4 简化条件表达式
1、分解条件表达式
- decompose conditional
- 动机
- 复杂的条件逻辑是最常导致复杂度上升的地点之一,
- 什么时候做
- 有一个复杂的条件语句
- 怎么做
- 从if,then,else三个段落中分别提炼出独立函数
- 将其分解为多个独立函数,根据每个小块代码的用途分解而得的新函数命名。
- 很多人都不愿意去提炼分支条件,因为这些条件非常短,但是提炼之后函数的可读性很强,就像一段注释一样清楚明白。
2、合并条件表达式
- consolidate conditional expression
- 其实就是用一个小型函数封装一下,小型函数的名字可以作为注释。
- 动机
- 合并后的条件代码会使得检查的用意更加清晰,合并前和合并后的代码有着相同的效果。
- 什么时候做
- 有一系列条件测试,都得到相同结果
- 怎么做
- 将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。
3、合并重复的条件片段
- consolidate duplicate conditional fragments
- 动机
- 什么时候做
- 在条件表达式的每个分支上都有着相同的一段代码
- 怎么做
- 将这段重复代码搬移到条件表达式之外。
4、移除控制标记
- remove control flag
- 动机
- 单一出口原则会迫使让妈中加入讨厌的控制标记,大大降低条件表达式的可读性,
- 什么时候做
- 在一系列布尔表达式中,某个变量带有"控制标记"(control flag)的作用
- 怎么做
- 以break语句或return语句取代控制标记
5、以卫语句取代嵌套条件表达式
- replace nested conditional with guard clauses
- 动机
- 单一出口的规则其实并不是那么有用,保持代码清晰才是最关键的。
- 什么时候做
- 函数中条件逻辑使人难以看清正常的执行路径
- 怎么做
- 使用卫语句表现所有特殊情况。
6、以多态取代条件表达式
- replace conditional with polymorphism
- 动机
- 如果需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显的条件表达式。
- 同一组条件表达在程序许多地点出现,那么使用多态的收益是最大的。
- 什么时候做
- 有一个条件表达式,根据对象类型的不同而选择不同的行为
- 怎么做
- 将这个体哦阿健表示式的每个分支放进一个子类的覆写函数中,然后将原始函数声明为抽象函数。
7、引入null对象
- introduce null object
- 动机
- 多态的最根本好处就是不必要想对象询问你是什么类型而后根据得到的答案调用对象的某个行为,只管调用该行为就是了。
- 空对象一定是常量,它们的任何成分都不会发生变化,因此可以使用单例模式来实现它们。
- 什么时候做
- 需要再三检查对象是否为null
- 怎么做
- 将null对象替换成null对象。
8、引入断言
- introduce assertion
- 动机
- 断言是一个条件表达式,应该总是为真,如果它失败,表示程序员犯了一个错误。因此断言的失败应该导致一个非受控异常(unchecked exception)。
- 加入断言永远不会影响程序的行为。
- 用它来检查一定必须为真的条件。
- 什么时候做
- 某一段代码需要对程序状态做出某种假设
- 怎么做
- 以断言明确表现这种假设
5 简化函数调用
所有的数据都应该隐藏起来。
1、函数改名
- rename method
- 动机
- 将复杂的处理过程分解成小函数。
- 什么时候做
- 函数名称未能揭示函数的用途
- 怎么做
- 修改函数名称
2、添加参数
- add parameter
- 动机
- 什么时候做
- 某个函数需要从调用端得到更多信息
- 在添加参数外常常还有其他的选择,只要有可能,其他选择都比添加参数要好(查询),因为它们不会增加参数列的长度,过长的参数列是一个不好的味道。
- 怎么做
- 为此函数添加一个对象参数,让该对象带进函数所需信息。
3、移除参数
- remove parameter
- 动机
- 可能经常添加参数却很少去除参数,因为多余的参数不会引起任何问题,相反以后可能还会用到它。请去除这些想法。
- 什么时候做
- 函数本体不需要某个参数
- 怎么做
- 将该参数去除。
4、将查询函数和修改函数分离
- separate query from modifier
- 动机
- 在多线程系统中,查询和修改函数应该被声明为synchronized(已同步化)
- 什么时候做
- 某个函数既返回对象状态值,又修改对象状态
- 任何有返回值的函数,都不应该又看得到的副作用。
- 常见的优化是将某个查询结果放到某个字段或集合中,后面如何查询,总是获得相同的结果。
- 怎么做
- 建立两个不同的函数,其中一个负责查询,另一个负责修改。
5、令函数携带参数
- parameterize
- 动机
- 去除重复代码
- 什么时候做
- 若干函数做了类似的工作,但在函数本体中却饱含了不同的值
- 怎么做
- 建立单一函数,以参数表达那些不同的值
6、以明确函数取代参数
- replace parameter with explicit methods
- 动机
- 避免出现条件表达式,接口更清楚,编译期间就可以检查,
- 如果在同一个函数中,参数是否合法还需要考虑
- 但是参数值不会对函数的行为有太多影响的话就不应该使用本项重构,如果需要条件判断的行为,可以考虑使用多态。
- 什么时候做
- 有一个函数,其中完全取决于参数值不同而采取不同行为
- 怎么做
- 针对该参数的每一个可能值,建立一个独立函数
7、保持对象完整
- preserve while object
- 动机
- 不适用完整对象会造成重复代码
- 事物都是有两面性,如果你传的是数值,被调用函数就只依赖于这些数值,如果传的是对象,就要依赖于整个对象。如果依赖对象会造成结构恶化。那么就不应该使用保持对象完整。
- 如果这个函数使用了另一个对象的多项数据,这可能以为着这个函数实际上应该定义在那些数据所属的对象上,应该考虑移动方法。
- 什么时候做
- 从某个对象中取出若干值,将它们作为某一次函数调用时的参数
- 怎么做
- 改为传递整个对象
8、以函数取代参数
- replace parameter with methods
- 动机
- 尽可能缩减参数长度
- 什么时候做
- 对象调用某个函数,并将所有结果作为参数传递给另一个函数,而接受该参数的函数本省也能够调用前一个函数。
- 怎么做
- 让参数接受者去除该项参数,并直接调用前一个函数。
9、引入参数对象
- introduce parameter object
- 动机
- 特定的一组参数总是一起被传递,可能有好几个函数都使用这一组参数,这些函数可能隶属于同一个类,也可能隶属于不同的类。这样的参数就是所谓的数据泥团,可以运用一个对象包装所有的这些数据,再以该对象取代它们。
- 什么时候做
- 某些参数总是很自然地同时出现
- 怎么做
- 以一个对象取代这些参数
10、移除设值函数
- remove setting method
- 动机
- 使用了设值函数就暗示了这个字段值可以被改变。
- 什么时候做
- 类中某个字段应该在对象创建时被设值,然后就不再改变。
- 怎么做
- 去掉该字段的所有设值函数。
11、隐藏函数
- hide method
- 动机
- 面对一个过于丰富、提供了过多行为的接口时,就值得将非必要的取值函数和设置函数隐藏起来
- 什么时候做
- 有一个函数,从来没有被其他任何类用到
- 怎么做
- 将这个函数修改为private
12、以工厂函数取代构造函数
- replace constructor with factory method
- 动机
- 使用以工厂函数取代构造函数最显而易见的动机就是在派生子类的过程中以工厂函数取代类型码。
- 工厂函数也是将值替换成引用的方法。
- 什么时候做
- 希望在创建对象时不仅仅是做简单的构建动作
- 怎么做
- 将构造函数替换为工厂函数
- 使用工厂模式就使得超类必须知晓子类,如果想避免这个可以用操盘手模式,为工厂类提供一个会话层,提供对工厂类的集合对工厂类进行控制。
13、封装向下转型
- encapsulate downcast
- 动机
- 能不向下转型就不要向下转型,但如果需要向下转型就必须在该函数中向下转型。
- 什么时候做
- 某个函数返回对象,需要由函数调用者执行 向下转型
- 怎么做
- 将向下转型动作移到函数中
14、以异常取代错误码
- replace error code with exception
- 动机
- 代码可以理解应该是我们虔诚最求的目标。
- 什么时候做
- 某个函数返回一个特定的代码,用以表示某种错误情况
- 怎么做
- 改用异常
- 决定应该抛出受控(checked)异常还是非受控(unchecked)异常
- 如果调用者有责任在调用前检查必要状态,就抛出非受控异常
- 如果想抛出受控异常,可以新建一个异常类,也可以使用现有的异常类。
- 找到该函数的所有调用者,对它们进行相应调整。
- 如果函数抛出非受控异常,那么就调整调用者,使其在调用函数前做适当检查,
- 如果函数抛出受控异常,那么就调整调用者,使其在try区段中调用该函数。
15、以测试取代异常
- replace exception with test
- 动机
- 在异常被滥用的时候
- 什么时候做
- 面对一个调用者可以预先检查的体哦阿健,你抛出一个异常
- 怎么做
- 修改调用者,使它在调用函数之前先做检查
6 处理继承关系
1、字段上移
- pull up field
- 动机
- 减少重复
- 什么时候做
- 两个子类拥有相同的字段
- 怎么做
- 将该字段移至超类
2、函数上移
- pull up method
- 动机
- 滋生错误
- 避免重复
- 什么时候做
- 有些函数在各个子类中产生完全相同的结果
- 怎么做
- 将该函数移至超类
- 最烦的一点就是,被提升的函数可能会引用子类中出现的特性,如果被引用的是一个函数可以将这个函数一同提升至超类,或则在超类中建立一个抽象函数。
- 如果两个函数相似但不相同,可以先借助塑造模板函数。
3、构造函数本体上移
- pull up constructor body
- 引用
- 如果重构过程过于复杂,可以考虑使用工厂方法。
- 什么时候做
- 在各个子类中拥有一些构造函数,它们的本体机会完全一致
- 怎么做
- 在超类中新建一个构造函数,并在子类构造函数中调用它。
4、函数下移
- push down method
- 动机
- 把某些行为从超类移动到特定的子类中。
- 什么时候做
- 超类中某个函数只与部分子类有关
- 怎么做
- 将这个函数移到相关的那些子类中
- 如果移动的函数需要使用超类中的某个字段,则需要将超类中的字段的开放protected.
5、字段下移
- push down field
- 动机
- 什么时候做
- 超类中的某个字段只被部分子类用到
- 怎么做
- 将这个字段移到需要它的那些子类去
6、提炼子类*?
- extract subclass
- 动机
- 类中的某些行为只被一部分实例用到,其他实例不需要,有时候这些行为上的差异是通过类型码分区的,可以使用子类替换类型码,或则使用状态或策略模式替代类型码。
- 抽象类和抽象子类则是委托和继承之间的抉择
- 抽象子类会更加容易,但是一旦对象建立完成,无法再改变与类型相关的行为。
- 什么时候做
- 类中的某些特性只被某些实例用到
- 怎么做
- 新建一个子类,将上面所说的那一部分特性移到子类中
- 为源类定一个新的子类
- 为这个新的子类提供构造函数
- 让子类构造函数接受与超类构造函数相同的参数,并通过super调用超类的构造函数。
- 用工厂替换构造函数
- 找出调用结果超类构造函数的所有地点,新建子类
- 下移方法和字段
7、提炼超类*?
-
extract superclass
动机
-
什么时候做
- 两个类有相似特性
-
怎么做
- 为这两个类建立一个超类,将相同特性移至超类。
-
新建一个空白抽象类
- 上移字段和方法
- 先搬移字段
- 子类函数中有相同的签名,但函数体不同,可以抽象函数
- 如果方法中有相同算法,可以使用提炼算法,将其封装到同一个函数中。
8、提炼接口
- extract interface
- 动机
- 类之间彼此互用的方式有若干种,某一种客户只使用类责任区的一个特定子集。
- 某个类在不同环境下扮演截然不同的角色,使用接口就是一个好主意。
- 什么时候做
- 若干客户使用类接口中同一个子集,或者两个类的接口有部分相同
- 怎么做
- 将相同的子类提炼到一个独立接口中。
- 上移字段和方法
9、折叠继承关系
- collapse hierarchy
- 动机
- 什么时候做
- 超类和子类之间无太大区别
- 怎么做
- 将它们合为一体
10、塑造模板函数
- form template method
- 动机
- 既避免重复也保持差异。
- 什么时候做
- 有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同。
- 怎么做
- 将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同的,然后将原函数上移至超类
11、以委托取代继承
- replace inheritance with delegation
- 动机
- 超类中有许多操作并不真正适用于子类,这种情况下,你所拥有的接口并未真正反映出子类的功能。
- 什么时候做
- 某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据
- 怎么做
- 在子类中新建一个字段用以保存超类,调整子类函数,令它改而委托超类,然后去掉两者之间的继承关系。
- 在子类中新建一个字段,使其引用超类的实例
- 修改子类中的所有函数,让它们不再使用超类,转而使用上述那个受托字段。
12、以继承取代委托
- replace delegation with inheritance
- 动机
- 如果并没有使用受托类的所有函数,就不应该使用用继承替换委托,
- 可以使用去除中间层的方法让客户端自己调用受托函数。
- 什么时候做
- 在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数。
- 怎么做
7 大型重构
1、梳理并分解继承体系
- tease apart inheritance
- 就是让每个类的职责更明确更单一,当一个类的职责混乱时,通过绘制职责图来分离职责,并创建另一个超类,将相关的字段和方法都移动到另一个超类
- 动机
- 混乱的继承体系是一个严重的问题,会导致重复代码,而后者正是程序员生涯的致命毒药。还会使修改变得困难,因为特定问题的解决决策被坟山到了整个继承体系。
- 什么时候做
- 某个继承体系同时承担两项责任
- 怎么做
- 建立两个继承体系,并通过委托关系让其中一个可以调用另一个
- 首先识别出继承体系所承担的不同责任,然后建立一个二维表格(或则三位乃至四维表格),并以坐标轴标示不同的任务,
- 判断哪一项责任更重一些,并准备将它留在当前的继承体系中,准备将另一项责任移到另一个继承体系中。
- 使用抽象类方法从当前的超类提炼出一个新类,用以表示重要性稍低的责任,并在原超类中添加一个实例变量,用以保存新类的实例。
- 对应于原继承体系中的每个子类,创建上述新类的一个子类,在原继承体系的子类中,将前一步骤所添加的实例变量初始化为新建子类的实例。
- 针对原继承体系中的每个子类,使用搬移函数的方法迁移到与之对应的子类中。
2、将过程化设计转化为对象设计
- convert procedural design to objects
- 动机
- 什么时候做
- 有一些传统过程化风格的代码
- 怎么做
- 将数据记录变成对象,将大块的行为分为小块,并将行为移入相关对象之中。
- 针对每一个记录类型,将其转变为只含访问函数的哑数据对象
- 针对每一处过程化风格,将该出的代码提炼到一个独立类中。
- 针对每一段长长的程序,试试提炼方法将长方法分解并将分解后的方法移动到相关的哑数据类。
3、将领域和表诉/显示分离
-
separate domain from presentation
-
动机
- mvc模式最核心的价值在于,它将用户界面代码(即视图:亦即现今常说的展现层)和领域逻辑(即模型)分离了,展现类只含用以处理用户界面的逻辑:领域类包含任何与程序外观相关的代码,只含业务逻辑相关代码,将程序中这两块复杂的部分加以分离,程序未来的修改将变得更加容易,同时也使用同意业务逻辑的多种展现方式称为可能。
-
什么时候做
- 某些gui类中包含了领域逻辑
-
怎么做
将领域逻辑分离出来,为它们建立独立的邻域类。
为每个窗口建立一个领域类,
如果窗口内含有一张表格,新建一个类来表示其中的行,再以窗口所对应之领域类中的一个集合来容纳所有行领域对象
检查窗口中的数据,如果数据只被用于ui,就把它留着,如果数据被领域逻辑使用,而且不显示与窗口上,我们就可以使用移动方法将它搬移到领域类中,如果数据同时被ui和领域逻辑使用,就对它实施复制被监视数据,使它同时存在于两处,并保持两处之间的同步。
展现类中的逻辑,实施提炼方法将展现逻辑从邻域逻辑中分开,一旦隔离了邻域逻辑,在运用搬移方法将它移到邻域类。
4、提炼继承体系
- extract hierarchy
- 动机
- 一开始设计者只想以一个类实现一个概念,但随着设计方案的演化,最后可能一个类实现两个三乃至十个不同的概念。
- 什么时候做
- 有某个类做了太多的工作,其中一部分工作是以大量条件表达式完成的
- 怎么做
- 建立继承体系,以一个子类表示一种特殊情况。
- 有两种重构的手法
- 无法确定哪些地方会发生变化
- 不确定哪些地方会发生变化
- 鉴别出一中变化情况,
- 如果这种拜年话可能在对象声明周期的不同阶段而有不同体现就用提炼方法将它提炼为一个独立的类
- 针对这种变化情况,新建一个子类,并对原始类实施工厂方法替代构造函数,再次修改工厂方法,令它返回适当的子类实例。
- 将含有条件逻辑的函数,一个个复制到子类
- 有必要隔离函数中的条件逻辑和非条件逻辑。
- 删除超类中那些被所有子类覆写的函数本体,并将它们声明为抽象函数。
- 鉴别出一中变化情况,
- 确定原始类中每一种变化
- 针对原始类中每一种变化情况,建立一个子类,
- 使用工厂方法替代构造函数将原始类的构造函数转变成工厂函数,并令它针对每一种变化情况返回适当的子类实例。
- 如果原始类中的各种变化情况是以类型码标示,使用子类替换类型码,如果那些变化情况在对象周期的不同阶段会有不同体现,使用状态和策略模式替换类型码
- 针对带有条件逻辑的函数,实施用多态替换条件如果非整个函数的行为有所变化,请先运行提炼方法将变化部分和不变部分分隔开来
8 案例
- 有一个影片商店客户端,需要计算每一个客户的消费,常客积分
- 客户customer
- 租赁rental
- 影片movie,普通regular,儿童children,新片release
- regular:2天内2元,大于2天1.5一天
- release:每天三元
- childrens:3天内1.5元,大于3天1.5一天
- 计费函数
提炼方法
这个计费函数太复杂
- 修改参数名
搬移方法
- amountfor没有使用customer任何信息,只是使用了rental类的,将其搬移到rental类中
- 修改原customer中函数调用
用查询替换临时变量
- 用同样的方法来处理计算常客积分的部分
使用多态替换switch
- 原来的switch,在rental类中
- 不要再另一个对象属性上使用switch,将其移动到方法中
- 移动过后
- 常客积分
- 继承机制
- 一个影片可以再生命周期内修改自己的分类,一个对象却不能再生命周期内修改自己所属的类,这里需要使用用状态或策略模式替换type,搬移方法到超类,用多态替换条件
- 提炼超类
- 修改原来movie中的getcharge方法,
- 首先getcharge移动方法
- 用多态替换getcharge方法