什么是重构
- 重构就是以微小的步伐修改程序,如果你犯下错误,便可很容易的发现它。
- 对软件内部的结构的一种调整,目的是不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
- 使用一系列重构手法,在不改变软件的可观察行为的前提下,对其结构进行优化。
- 重构的目的是让让软件更容易被理解和修改,与之对比的是性能优化,和重构一样,性能优化通常不会改变组件的行为(除了执行速度),只会改变其内部结构,但是两者的出发点不同,性能优化往往让代码更难理解,但为了你所需的性能你不得不那么去做。
- 重构不会改变软件的可观察行为,重构之后软件的功能一如既往。
重构的目的:
- 重构改进软件设计,防止长时间开发积累的脱离结构的代码导致程序腐败
- 重构使软件更加容易理解 "擦掉窗户上的污垢,使其看的更远"
- 重构可以帮助找到Bug
- 重构提升编程速度(良好的设计是快速开发的根本)
重复的做类似的事情?事不过三,三则重构
何时重构:
- 添加功能的时候重构 重构让我更加理解这段代码,重构让我弥补之前的不足,让添加新特性更加轻松。
- 修补错误的时候重构 发现Bug是这段代码需要重构的信号,说明其结构不够清晰,清晰到我没能一眼看出它有bug
- 复审代码的时候重构
重构的原则:
- 减少不必要的间接层: 如一个有着多态性的组件,但是最终其实只有一处用到了,请将这种寄生虫式的间接层去掉
- 合理的引入间接层:
- 间接层的好处:
- 允许逻辑共享
- 分开解释意图和实现
- 隔离变化
- 封装条件逻辑
- 何时引入间接层?
- 找到缺乏“间接层好处的地方”,不修改其现有行为的前提下引入间接层
- 间接层的好处:
- 对比事前设计的优势:小心翼翼的事前设计让整个系统的完成之前就拥有了极高的质量,程序员只需要讲代码塞进整个强健的模板/骨架就可以了,但也有非常大的隐患:太容易猜错,理解上的错误会导致全盘的错误,但重构永远面临全盘出错的风险。程序从始至终都是能保持一致的。
重构的难题
数据库
难点:程序与背后的数据库过分耦合在一起 解决方法:在对象模型和数据库模型中插入一个分层。 优势:虽然增加了复杂度,但是使得程序变得更加灵活,但无需一开始就引入,在对象变得不再稳定的时候再行引入即可。
修改接口
难点:接口改变了,任何事情都可能发生: 如果这个函数的(哪怕是public)所有的调用者都在你的控制下,即使您将函数名称改掉了也能获取其调用者进行修正的时候,是不存在问题的,但是这个接口是个“已经发布的接口”,我们不能在对其调用者进行修改,或者根本无法获得其调用者的情况下,这时候才是问题。 解决方法: 1. 发布新旧两个接口(将旧接口进行deprecated标记) 缺点: 必须构造并且维护一些额外的函数,这让接口变得复杂且难以调用。 2. 尽量不发布没有必要的接口 3. 在throw子句之中添加自定义的异常:为这个新的函数指定名称,并在旧的函数中调用它,将这个受控异常变为非受控异常。
难以通过重构进行改变的设计改动
想象重构的情况,如果有简单的设计方式作为重构对象,可以先重构为简单的设计,及时不能覆盖全部的需求。如果没有,则需要重新在设计上花时间。
何时不该重构
- 代码过于混乱以至于重写更加简单时候,别犹豫,重写吧。
- 项目的末期应该避免重构,重构任务相当于债务,应该是及时写及时重构,拖得太久终会被利息(代码质量腐败)压垮。重构可以提高生产力,但如果最后没有时间进行重构,表明你早该开始重构了。
重构与设计
- 重构和设计互为补充
- "预先设计”可以减少反工的时间
- “有了设计我们可以思考的更快,但是其中有许多小漏洞”
- 重构的另辟蹊径:预先设计并不需要多么正确复杂仅需足够合理,在实现的过程中对问题逐渐理解加深之后,发现最佳解决方案与“预先设计”不同之时,重构让日后的修改成本不在高昂。
- 简化了设计成本:灵活的设计方案成本太大,因为个性的问题分布在程序的各个角落,在所有地方建立起灵活性,复杂度和可维护性难度会直接飙升。实践阶段发现有些灵活性毫无必要,是不可避免的,为了获得这种灵活性,我们在实际行动需要更多的灵活性。
- 重构的途径来解决这种风险,我们只需要思考“如何把目前简单的实现方案重构成灵活的实现方案”。
劳而无获
Ron Jeffries关于克莱斯勒综合薪资系统: “哪怕我们十分了解系统,也请实际度量它的性能,不要臆测,臆测会让你学到一些东西,但十有八九是错的。"
重构与性能
- 不赞成为了设计的纯洁性而忽视性能
- 重构使得性能优化变得更加容易,虽然重构短期内可能导致运行速度的变慢:“先写出可调的软件,再去优化其性能获取足够速度”
重构的起源
坏代码的“味道”
“如果尿布丑了,就换掉它。”
重复代码
- 同一个类里面有相同的函数 解决方案:采用“Extract Method”(一些名词后续进行解释)提炼出统一的代码,然后再原使用处进行调用。
- 互为兄弟的子类之中,有相同的表达式 解决方案:对两个类进行“Extract Method”,在之后对提炼出的代码进行“Pull Up Method”,将其推入父类之内。如果仅仅是类似,运用“Extract Method”将相似部分与差异部分分隔开,构成一个单独的函数,然后运用“From Template Method”获得一个Template Method设计模式。 -如果函数以不同的算法做相同的事情 解决方案:选择其中较为清晰的一个,并使用“Subsitute Algorithm”将其他的函数算法替换掉
- 两个毫无相干的类中出现重复代码 解决方案:考虑对其中一个类使用“Extract Class”,将重复代码提炼到一个独立类中,然后在另一个类中使用这个新类。也可以根据实际情况,确实应当属于其中一个类时,自行决定放置位置。
过长的函数
原则:当我们感觉需要以注释来说明点什么的时候,我们就需要把需要说明的东西封进一个独立函数之中,并以其用途(而非实现手法)命名。 函数的长度可能会增加,但语义从 如何做--->做什么 大部分场合,缩小函数只需要提炼出函数中适合集中的部分并成一个新的函数。
- 函数内有大量的参数和临时变量
- 经常性的使用“Replace Temp With Query”来消除这些临时元素。
- “Introduce Parameter Object”和“Preserve Whole Object”使得过长的参数变得简洁。
- “Replace Method With Method Object”。
- 条件表达式和循环也是可提炼的信号
过大的类
导致原因:试图使用单个类做太多的事情 解决方案:可以运用“Extract Class”将几个变量存到一个新类中去,如果变量中的词缀和词头有所重叠,尝试用“Extract SubClass”提炼到某个组件或者子类之中。 小技巧:可以先确定客户端如何使用他们,之后运用“Extract interface”为每种方式提供一个接口,这可以帮助分解这个类。
过长的参数列
如果可以香已存在的对象(这个对象代表另一个参数,或者class对象)发送请求就能获取到这个参数,可以使用“Replace Parameter with Method”重构,也可以将这些参数封装为一个“参数对象”,以参数对象代替过长的参数列。 如果不希望对象与大对象之间发生依赖关系,那就该考虑结构是否合理了。
发散式的变化
解释:某个类常常因为各种原因在不同的方向上产生不同的变化,不利于修改 针对某一外界变化的所有对应的修改,都应该发生在单一类中,而这个新类的所有内容都反应这个变化。 解决方案:找出特定原因下的所有变化,然后使用“Extract Class”将他们提取到另一个类中。
发散式的修改
情景:遇到某种情况,都必须在许多不同的类中做小修改。这回导致代码四散在各处,导致遗漏需要修改的地方。 解决方案:这种情况下,应该使用“Move Method”和“Move Fileld”将所有需要修改的代码放进同一个类 上面两种情况目的都是为了让外界的变化和变动的类 一一对应
依恋情结
情景:一个函数从另一个类中调用大量的值/一个函数用到了许多类的内容 解决方案:使用“Move Method”将函数提取到它该去的地方,如果只是函数中的一部分需要这种依恋关系,可以使用“Extract Method”提炼到独立函数中,在使用“Move Method”。
数据泥团
删除数据项中大量数据中的其中一个,如果其他数据因此失去意义,可以考虑为他们创建一个新对象。
Primitive Obsession(基本类型偏执)
尝试使用一些小对象代替传统的基本类型
Switch的惊悚现身
少用switch语句 尝试使用多态来代替它,如果是在单一函数之中,可以使用“Replace Parameter with Explicit Method”,如果选择条件含null,试试看“Introduce Null Object”。
平行继承体系
情景:某个类是另一个类的特殊情况 导致的问题:你更改一个类的时候,必须也对另一个类做对应的修改 如何消除:让一个继承体系的实例去引用另一个继承体系的实例
Lazy Class
某些子类没有做足够的工作,使用“Collapse Hierarchy” 对于几乎没用的组件,使用“Inline Class”对付他们
夸夸其谈的未来性
情景:为了将来“可能需要处理”的某个事情,而用各种各样的钩子,特殊情况来处理。 某个抽象类其实并没有太大作用 解决方案:一刀删掉
令人迷惑的暂时字段
情景:一个对象中的某个字段仅仅是为了某种特殊情况而设置的,而不是所有情况都使用的一个通用型的变量。
解决方案:可以使用Extract Class的方法,将这种变量安置在一个新的类中,并将是由与之相关的代码迁移进去,并可以在某些情况下使用Introduce Null Object,在变量不合法的情况下创建出一个空的对象,从而避免写出条件式的代码。
情景:一个函数实现了个复杂算法,同时需要传入很多个变量。
解决方案:在不希望出现长函数的地方,使用Extract Class的方法,将这些参数独立在一个类中,使用此算法函数时,传入对象代替传入长串变量。
过度耦合的消息链
情景:向一个对象请求另一个对象,后者会向第三个对象去请求第四个对象,这就是消息链; 一旦中间环节对象之间的关系有所变化,客户端将不得不进行相应的修改。
解决方案:观察消息链的最终端的结果,将这一系列的过程使用Extract Method的方式进行封装,再之后Move Method将进行替换。
中间人(Middle Man)
情景:一个类的大部分功能都交给第二个类去执行,自身的不对自己的大部分功能进行负责。
解决方案:将这个类转化为其使用的委托类的子类,这样子可以避免许多的委托操作的同时也能对原来的对象功能进行拓展,或者将这些方法抽离到实际负责的对象中去。
过于亲密的关系
情景:类和类之间的数据交流关系过于亲密,以至于很难去分清哪些是私有部分。
解决方案:将他们的共同点提取到一个新的类中。
异曲同工的类
情景:同作用但是名称却不同的函数
解决方案:根据他们的用途重新命名,将一些行为移入类中,如果其中有冗余重复操作,可以提取出一个类作为他们的共同的父类。
不完美的类库
情景:第三方库的功能不够完善,需要对类库的一些函数进行修改。
解决方案:只修改几个函数,使用Introduce Foreign Method,如果有大量的函数需要修改,使用Introduce Local Extension
纯稚的数据类(Data Class)
情景:纯粹的数据类,职能只有访问类其中的字段。
解决方案:将一些对这个类某些字段的处理的函数迁移到数据类中来,让这个类承担一定的职能。
被拒绝的馈赠
情景:子类只使用了父类的几个函数和数据,父类的绝大部分函数和数据是无用的。
解决方案:使用中间类或者兄弟类替代这个父类的子类,这个子类的继承关系是失败的。
过多的注释
当你感觉需要撰写注释的时候,请先尝试重构,试着让所有的注释都显得多余。
- 当你需要解释一片代码需要做什么的时候,使用Extract Method将这部分提取出来。
- 函数已经提炼出来,仍然需要注释,使用Rename Method将这个函数重新命名。
- 如果需要注释说明某些系统的规格需求,可以试试看Introduce Assertion。
- 对于没有把握的区域,写下“为xxx做xxx事”,便于其他人的修改。
构筑测试体系
重构的前提是有一个稳定可靠的测试环境。
自测代码的价值
确保所有的测试都自动化,让它们检测自己的测试结果。
重构列表
重新组织函数
-
提炼函数
-
内联函数
-
内联临时变量
-
以查询代替临时变量
-
引入解释型变量
-
分解临时变量
-
移除对参数的赋值
-
以函数对象取代函数
-
替换算法
在对象之间搬移特性
-
搬移函数
-
搬移字段
-
提炼类
-
将类内联化
-
隐藏委托关系
重新组织数据
- 自封装字段
- 以对象取代数据值
- 将值对象改为应用对象
- 将引用对象改为值对象
- 以对象取代数组
- 复制被监视数据
- 将单项关联改为双向关联
- 将双向关联改为单向关联
- 用字面常量取代魔法数
- 封装字段
- 封装集合
- 用数据类取代记录
- 用类取代类型码
- 以子类取代类型码
- 以State/Strategy取代类型码
- 用字段取代子类
简化条件表达式
- 分解条件表达式
- 合并重复的条件片段
- 移除控制标记