一、目的:
1、去除重复、简化逻辑、澄清模糊的代码
2、增加代码的可控性
3、简化对代码的理解
4、清除代码中的隐藏Bug
5、提高编程的速度
6、改善现有的设计,易于扩展
7、......(更多)
二、为什么要重构?
在本书的第3章:代码的坏味道(P75)具体列出了22种不同的需要重构的例子。
总结归纳为:
1、代码存在重复(可能是大量或少量)
2、逻辑处理过程复杂(一个过程包含了N个逻辑)
3、代码混乱模糊(强耦合、强依赖关系、隐藏BUG)
4、难于理解的代码段(过长、过大的类、函数体、条件表达式等)
5、.......(更多)
正是由于代码存在以上的问题,所以我们需要重新审视代码、对代码进行重新组织。那么我们如何做呢?
三、重构,我们要怎么做?
要考虑重构的做法,本书的第6-12章都在讲重构的手段,也举出了具体的例子(不过是Java的)那么我们都需要考虑那些特性?要从哪些地方开始入手呢?。
前提:重构是建立在健壮的测试基础之上的 (测试驱动开发)
总结归纳入下:
1、函数
2、类(子类,继承类,变量类及类的多态和继承特性)
3、数据
4、条件表达式(if-elif-elif-....、switch-case-case-....)
5、变量(临时变量,全局变量)
6、对象
7、......(更多)
共性:重复,太长,表达不清晰,混乱,难于理解和阅读。
解决步骤:1、检查 2、声明 3、内联和提炼(上移或下提)
4、编译,测试 (3,4,3,4,3,4,3,4.......)
5、找出与此相关的引用点并修改 5、删除 6、编译,测试
7、......(更多)
对第3章提到的代码的坏味道,本书也给出了一页第6-12章处理这些坏味道的表格,此表格非常方便,方便做为以后指导用,如下是从网上找了一份归纳图,个人感觉更有实际意义,也可以看本书的最后一页(不过页码比较麻烦,转来转去):
坏味道 | 特征 | 情况及处理方式 | 目标 | ||
---|---|---|---|---|---|
重复代码 | 1.重复的表达式 2.不同算法做相同的事 3.类似代码 | 同一个类的两个函数有相同表达式 | 重复代码提取为方法 | 相同表达式只在一个类的一个方法出现,供其他方法调用 | |
兄弟类含有相同表达式 | 重复代码提取为方法 提升方法到父类 | ||||
不相干类含有相同代码 | 提取为独立类供调用 | ||||
过长函数 | 1.代码前面有注释 2.条件表达式 3.循环 | 提取方法 | 每个方法只做一件事,方法要定义完善、命名准确 | ||
过大的类 | 1.一个类中有太多实例变量 2.一个类中有太多代码 | 部分字段之间相关性高 | 相关的字段和方法提取为类 | 每个类负责一组具有内在的相互关联的任务 | |
某些字段和方法只被某些实例用到 | 这些字段和方法移到子类中 | ||||
过长参数列 | 1.参数列过长 2.参数列变化频繁 | 方法可以通过其他方式获取该参数 | 让参数接受者自行获取该参数 | 只需要传给函数足够的、让其可以从中获取自己需要的东西就行了 | |
同一对象的若干属性作为参数 | 在不使依赖恶化的情况下,使用整个对象作为参数 | ||||
被调用函数使用了另一个对象的很多属性 | 将方法移动到该对象中 | ||||
某些数据缺乏归属对象 | 首先创建对象 | ||||
发散式变化 | 一个类受多种变化的影响 | 类经常因为不同的原因在不同的方向上发生变化 | 将特定原因造成的所有变化提取为一个新类 | 针对某一外界变化的所有修改,只应发生在单一类中,而这个类中所有的内容都应反映此变化 | |
散弹式修改 | 一种变化引发多个类的修改 | 某种变化需要在许多不同的类中做出小修改 | 把所有需要修改的代码放进同一个类中 | 针对某一外界变化的所有修改,只应发生在单一类中,而这个类中所有的内容都应反映此变化 | |
依恋情结 | 一个函数使用其他类属性比使用自身类属性还要多 | 某个函数从另一个对象调用了几乎半打的取值函数 | 将依恋代码提取为单独方法,移动到另一对象 | 将数据和对数据的操作行为包装在一起 | |
数据泥团 | 同时使用的相关数据并未以类的方式组织 1.两个类中相同的字段 2.许多函数中相同的参数 | 先将字段提取为类,再缩减函数签名中的参数 | 总是绑在一起的数据应该拥有属于它们自己的对象 | ||
基本类型偏执 | 过多使用基本类型 | 总是被放在一起的基本类型字段 | 提取类 | 将单独存在的数据值转换为对象 | |
参数列中有基本类型 | 提取参数对象 | ||||
数组中容纳了不同的对象,需要从数组中挑选数据 | 用对象取代数组 | ||||
基本数据是类型码 | 使用类替换类型码 | ||||
带条件表达式的类型码 | 使用继承类替换类型码 | ||||
Switch语句 | 相同的switch、case语句散布于不同地方 | 根据类型码进行选择的switch | 使用多态替代switch | 避免到处做相同的修改 | |
单一函数中有switch | 使用显式的方法取代参数 | ||||
平行继承体系 | 1.为某个类增加子类时,必须为另一个类增加子类 2.某个继承体系类名前缀和另一个继承体系类名前缀相同 | 一个继承体系中的实例引用另一个继承体系中的实例,然后迁移成员 | 避免到处做相同的修改 | ||
冗赘类 | 类无所事事 | 父类和子类无太大差别 | 将它们合为一体 | ||
某个类没有做太多事情 | 将这个类所有成员移到另一个类中,删除它 | ||||
夸夸其谈未来性 | 某个抽象类没有太大作用 | 将父子类合并 | |||
不必要的委托 | 将这个类所有成员移到另一个类中,删除它 | ||||
函数的某些参数未用上 | 移除参数 | ||||
函数名称带有多余的抽象意味 | 重命名函数名 | ||||
函数只被测试方法调用 | 连同测试代码一并删除 | ||||
令人迷惑的暂时字段 | 1.某个实例字段仅为某种情况而设 2.某些实例字段仅为某个函数的复杂算法少传参数而设 | 提取单独的类,封装相关代码 | |||
过度耦合的消息链 | 一长串的get_this或临时变量 | 客户类通过一个委托类来取得另一个对象 | 隐藏委托 | 消除耦合 | |
中间人 | 某个类接口有大量的函数都委托给其他类,过度使用委托 | 有一半的函数 | 移除中间人 | ||
少数几个函数 | 直接调用 | ||||
中间人还有其他行为 | 让委托类继承受托类 | ||||
狎昵关系 | 某个类需要了解另一个类的私有成员 | 子类过分了解超类 | 将继承改为委托,把子类从继承体系移出 | 封装 | |
类之间双向关联 | 去掉不必要的关联 | ||||
类之间有共同点 | 提取新类 | ||||
异曲同工的类 | 两个函数做同一件事,但是签名不同 | 合并 | |||
不完美的类库 | 类库函数构造的不够好,又不能修改它们 | 想修改一两个函数 | 在调用类增加函数 | ||
想添加一大堆额外行为 | 使用子类或包装类 | ||||
幼稚的数据类 | 某个类除了字段,就是字段访问器、设置器 | 1.用访问器取代public字段 2.恰当封装集合 3.移除不需要的设置器 4.搬移对访问器、设置器调用方法到此类 5.隐藏访问器、设置器 | 封装 | ||
被拒绝的馈赠 | 派生类仅使用了基类很少一部分成员函数 | 子类拒绝继承超类接口 | 使用委托替代继承 | ||
过多的注释 | 一段代码有着长长的注释 | 消除各种坏味道 |
三、重构能给我们带来什么?
个人感触如下:1、扩展更容易 2、增加代码的可控性,理解更透彻
3、代码更优秀,结构更清晰 4、保证较低的Bug率
5、提高编程的速度 6、代码质量得以保证
7、团队开发更便捷 8、......(更多)
四、疑问点:
1、应该通过重构实现模式、趋向模式和去除模式,而不是在预先设计时就使用模式?
本书中讲先实现代码,再趋向于模式,就是重构的过程,这个与现在的思维相反,一般情况下:我们是先设计,再实现。
2、大量的测试环境的构筑,是否会影响开发的进度?
本书中提到要先建立健壮的测试环境,然后再进行编码。而重构的步骤是:测试-》小修改-》再测试-》重复小修改的过程...(持续重构)。
3、尽可能的让函数返回一个值?
本书中前提是要采用减少临时变量的重构方法之后,如果减少临时变量的重构方法不能改变的话,那就用方法对象来替换函数,因为在对象里的临时变量可以有很多。
4、在使用重构时,每种方法的适当应该之处,例如Extract Method和 Introduce Explaining Variable两种方法都能准确的表达重构的意图,如何使用看个人爱好?还是?
这个比较难以定义,看完整本书后,基本上清晰,两种都可以用,一步步的提炼,没有最好,只有更好吧!
扩展:模式是重构(必经之路)希望达到的目标,也就是说设计模式是重构的目的。
这个东西需要好好理解下,另外,重构中涉及到的工厂模式、策略模式等,都是一知半解,还需要慢慢消化。