重构,改善既有代码的设计

重构的定义:
是在不改变软件可观察行为的前提下改善其内部结构
是在代码写好之后改进它的设计

重构的开始
如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便的达成目的,那就先重构那个程序。使特性的添加比较容易进行,然后再添加特性。

重构的技术支持
好的测试是重构的根本。为即将修改的代码建立一组可靠的测试环境。这些测试必须有自我检验的能力。
重构手法列表

重构的原则
最好不要在另一个对象的属性基础上运用switch语句。如果不得不使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用。

重构手法列表

状态模式

我们有数种影片类型,他们以不同的方式回答相同的问题。这听起来很像子类的工作。我们可以建立Movie的三个子类。每个都有自己的计费方法。
上面想法的问题是:一部影片可以在生命周期内修改自己的分类,一个对象却不能在生命周期内修改自己所属的类。
这个时候就可以使用状态(State)模式。

状态模式:
不仅用类可以表示对象,也可以用类表示状态。我们可以切换不同的类来表示不同的状态。
如果在一个类中有几种状态,在方法的调用中就需要判断不同状态下的不同做法。而在这个时候就可以使用状态模式。用不同的类表示不同的状态,我们只需要取得状态的委托调用他们就好了。不用关心他们的具体实现是什么。

推荐:尽量少用 if-else。
如果超过3层的if-else,请使用状态模式。

重构原则
重构的名词概念:对软件内部结构的一种调整,在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

和性能优化的区别:性能优化往往使代码难以理解,但为了性能 你不得不那么做。

两顶帽子:
“添加新功能”和“重构”
添加新功能时,不应该修改既有代码,只管添加新功能。
重构时就不能添加功能,只管改进程序结构。

重构很像是整理代码,你所做的就是让所有东西回到应处的位置上。

完成同样一件事,设计不良的代码往往需要更多的代码,因为代码在不同的地方使用完全相同的语句做同样的事情。因此改进设计的一个重要方向就是消除重复代码。这个动作的重要性在于方便未来的修改。

重构使软件更容易理解,在此基础上,程序员要修改某段代码的成本就会很低。

重构可以帮忙找到bug.在对代码重构的同时,可以深入理解代码的做为,并恰到好处的把新的理解反馈回去。然后通过搞清楚自己所做的一些假设,找出隐藏的bug。

良好的设计是快速开发的根本。重构就是把代码进行重新设计,防止系统腐败变质,提高设计质量。

何时重构

不需要特别拨出时间进行重构。重构应该随时随地进行。之所以重构,是因为你想做什么事,而重构能帮你帮那件事做好。

三次法则:
第一次做某件事只管去做
第二次做类似的事情还是可以去做;
第三次还是做同样的事情,就应该重构。

1.添加功能时重构
如果代码的设计无法帮助我轻松添加我所需要的特性

2.修补错误时重构
如果收到一份错误报告,这就是需要重构的信号,因为代码没有清晰到你可以一眼看出bug。

3.复审代码时重构
通过别人的眼光看自己的代码,接纳别人提出的有效意见

程序有两面价值:“今天为你做什么”和“明天为你做什么”。大多数时候,我们只关注自己今天想要程序做什么。对于今天的工作,我了解的很充分;对于明天的工作,我了解的不够充分。但如果我纯粹只是为今天工作,明天我将无法工作。

重构是一条摆脱困境的道路。如果你发现昨天的决定已经不适合今天的情况,放心改变这个决定就好。

间接层的应用

允许逻辑共享

如果重构手法改变了已发布接口,你必须同时维护新旧两个接口,直到所有用户都有时间对这个变化做出反应。
可以这么做:让旧接口调用新接口。将旧接口标记为deprecated。

重构与设计

事先做好设计可以节省返工的高昂成本

有一种观点认为:重构可以取代预先设计。先按最初想法开始编码,让代码有效运行,然后再进行重构。

在有了预先设计,然后开始实现的时候,你会对问题的理解逐渐加深,可能会察觉你的解决方案和当初设想的有些不同。不过只要有重构这把利器在手,就不成问题,可以用重构把软件设计趋于完美。

很多时候的程序设计在提高灵活性的同时也使得程序的复杂度和维护难度大大提高。

有了重构,你就可以通过一条不同的途径来应对变化带来的风险。不必预先思考前述所谓的灵活方案--一旦需要他,你总有足够的信心去重构。当下只管建造可运行的最简化系统,至于灵活而复杂的设计,多数时候你都不会需要它。

重构与性能

虽然重构可能使软件运行更慢,但它使软件的性能优化更容易。我们可以首先写出可调的软件,然后调整它以求获得足够的速度。

我们一视同仁的优化所有的代码,可是高频率执行的代码可能只有10%。

在性能优化阶段,首先应该用一个度量工具来监控程序的运行,让他告诉你哪些地方消耗更多的时间和空间。然后把这些性能热点所在的代码进行优化。

一个构建良好的代码可以帮助你进行性能优化。
1.更快速的添加功能,有更多的时间用在性能问题上。
2.有较细的粒度分析性能,度量工具可以把你带到范围较小的代码段落中。

什么时候重构

Duplicated Code

Long Method

程序愈长愈难理解。让小函数容易理解的关键在于一个好名字。读者可以通过名字理解函数的作用,根本不用去看其中写了些什么。

如果函数内有大量的参数和临时变量,他们会对你的函数提炼形成阻碍。

1寻找注释

2条件表达式和循环常常是提炼的信号.将循环和其中的代码提炼到一个独立函数中

Large Class

Long Parameter List

将对象传递给函数

Divergent Change (发散式变化)
指的是“一个类受多种变化的影响”
一旦代码需要修改,我们希望能够跳到某一点,只在该处修改。如果不能这样,就是要改变的时候了。

针对外界某一变化的所有相应修改,都只应该反应在单一类中,而这个类内的所有内容都应该反应此变化。找出某特点原因造成的所有变化,把他们提炼到另一个类中。

Shotgun Surgery
指的是“一种变化引发多个类相应修改”
如果需要修改的代码散布四处,就把这些需要修改的代码放进同一个类中。
这种情况下,我们整理代码,使“外界变化”与“需要修改的类”趋于一一对应。

Feature Envy(依恋情结)

对象技术:将数据和对数据的操作行为包装在一起的技术

一个函数中用到很多其他类的内容

Data Clumps(数据泥团)

两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起的出现的数据真应该拥有属于他们自己的对象。

不必在意data clumps只用上新对象的一部分字段,只要以新对象取代两个(或更对)字段,就值回票价了。

在拥有新对象之后,你就可以着手寻找Feature Envy。

Primitive Obsession(基本类型偏执)

将原本单独存在的数据值替换为对象。

Switch Statements

面向对象的程序要少用switch或(case)语句。从本质上说,switch语句的问题在于重复。面向对象的多态概念可为此带来优雅的解决办法

1.将switch语句提炼到一个独立函数中
2.将这个函数搬移到需要多态性的那个类中
这个时候你就可以决定是否使用 replace type code with subclasses 或者 replace type code with state/strategy
然后你就可以运用replace conditional with polymorphism

Parallel Inheritance Hierarchies(平行继承体系)

Shotgun Surgery的特殊情况
每当为一个类增加子类,必须也需要为另一个类相应增加子类的时候

消除这种重复性的策略:让一个继承体系的实例引用另一个继承体系的实例。

Lazy Class

一个类的所得不值其身价,就让他消失。

Speculative Generality(夸夸其谈未来性)

Temporary Field(令人迷惑的暂时字段)

某个字段仅为某些特殊情况存在。
把这样的字段和相关的代码放进一个新对象里面。提炼后的新对象将是一个函数对象。

Message Chains (过度耦合的消息链)

Middle Man

对象的基本特征就是封装--对外部世界隐藏其内部细节,封装往往伴随着委托。
如果某个类接口一半的函数都交给其他类,就是过度使用委托。

Inappropriate Intimacy(狎昵关系)

Aliternative Classes with Different Interfaces(异曲同工的类)

Incomplete Library Class(不完美的库类)

Data Class(纯稚的数据类)
不要有public字段
恰当的封装容器类的字段
对于不该被其他类修改的字段,使用readonly

找出这些取值/设值函数被其他类运用的地点,尝试搬移叨叨Data Class类中。

Refused Bequest(被拒绝的馈赠)

Comments(过多的注释)

如果代码有着长长的注释,通常是因为代码很糟糕。

如果需要注释来解释一块代码,试试 Extract Method.
如果函数已经提炼,还是需要注释,试试Rename Method
当你感觉需要注释时,请先尝试重构。

构筑测试体系

自测试代码的价值:

修复错误通常是比较快的,但找出错误却是噩梦一场。
"类应该包含它们自己的测试代码"

一套测试就是一个强大的bug侦测器,能够大大缩减查找bug所需要的时间。

撰写测试代码的最有用时机是在开始编程之前。

在重构之前先确保代码能够进行自我测试。

频繁的进行测试。每次编译请把测试页考虑进去,每天至少执行每个测试一次。

编写测试代码时, 一开始先让它们失败,测试一下测试代码的机制可以运行。

程序员的好朋友 :单元测试。
单元测试是高度局部化的东西,每个测试类都属于单一包。
功能测试用来保证软件可以正常运作。

每当收到一个bug报告,请先写一个单元测试来暴露这个bug

观察类该做的所有事情,然后针对任何功能的任何一种可能失败情况,进行测试

测试的要诀是:测试你最担心出错的部分。

每次测试都调用setUp()和tearDown() 是很重要的,因为这样才能保证测试之间彼此隔离。也就是说我们可以按任意顺序进行他们,不会对他们的结果造成任何影响。

测试的一项重要技巧就是“寻找边界条件”

考虑可能出错的边界条件,把测试火力集中在那儿。

当事情被认为会出错时,别忘了检查是否抛出了预期的异常。

单元测试的优点:
1.便于后期重构
2.优化设计
3.文档记录
4.具有回归性
5.带来自信
6.快速反馈
7.节省时间

设计模式,为重构行为提供了目标。模式是你希望达到的目标,重构则是到达之路 。

重构手法:

1.Extract Method.
好处:函数复用的机会更大
使高层函数更好阅读
函数的覆写也要容易

只有当你能给小函数很好的命名时,他们才能真正起作用,所以你需要在函数名称上下功夫。

1.创造一个新函数,以这个函数“做什么”来命名。
2.检查提炼出的代码,是否引用了作用域限于原函数的变量
3.检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将他们声明为临时变量。
4.检查被提炼代码段,是否有任何局部变量的值被他改变。如果有,就把提炼代码端处理为一个查询,并将结果赋值给相关变量

如果很难这么做,或者被修改的变量不止一个,就使用 Split Temporary Variable。

将被提炼代码段中需要读取的局部变量,当做参数传给目标函数。

replace Method with Method Object[https://www.cnblogs.com/rickyk/p/4156767.html]

Inline Temp(内联函数):
现象:一个函数的本体与名称同样清晰易懂。
做法:在函数调用点直接使用函数其中的代码,然后移除该函数。

replace temp with query(以查询取代临时变量)

临时变量保存某一表达式的运算结果。
将表达式提炼到一个函数,返回表达式结果,赋值给临时变量。
好处:可以在类中的各个地方获取计算之后的值。避免写出超长函数

如果你想替换的变量是用来收集结果的(例如循环中的累加值),就需要将某些程序逻辑(例如循环)复制到查询函数中去。

对于如此带来的性能问题,你可以现在不管他,因为它有可能不会带来任何影响。如果有影响,就在优化时期解决它。代码组织良好,往往能够发现更有效的优化方案。

简单情况:
临时变量只被赋值一次

1.将“对该临时变量赋值”之语句的等号右侧部分提炼到一个独立函数中。
2.确保提炼出来的函数不修改任何对象内容。如果有,就对他进行 Separate Query from Modifler
3.在该临时变量身上实施inline temp

Introduce Explaining Variable (引入解释性变量)

将一个复杂的表达式的结果放进一个临时变量,以此变量名称解释表达式用途。

Split Temporary Variable (分解临时变量)

某个临时变量被赋值超过一次, 它既不是循环变量,也不被用于收集计算结果。
针对每次赋值,创造一个独立、对应的临时变量

如果稍后之赋值语句是[i = i+某表达式],就意味着这是个结果收集变量。这种变量的作用通常是累加,字符串接合,写入流或者向集合添加元素。

Remove Assignments to Parameters(移除对参数的赋值)
以一个临时变量取代该参数的位置

把参数传递当做按值传递来做。

replace Method with Method Object

Substitute Algorithm(替换算法 )

Move Field(搬移字段)

Introduce Local Extension(引入本地扩展)

重新组织数据

1.自封装字段
2.replace data value with object(以对象取代数据值)
3.change value to reference (将值对象改为引用对象)
希望给一个值对象加入可修改的数据,并确保给一个对象的修改都能影响到所有引用此一对象的地方

4.replace array with object

需要图书PDF的可以留言

posted @ 2019-06-28 09:26  书院柯浩然  阅读(814)  评论(0编辑  收藏  举报