重构 改善既有代码设计读书笔记

前要

重构是什么? 这是我阅读这本书的初衷

在我感觉中 我们的系统总在迭代需求 写不下去就换个新系统,而重构貌似接近于重写与日常需求迭代中的一个中庸之道。或许可以帮助我们寻找一些新的工作范式

首先是重构的定义:

对软件内部结构的一种调整。目的是在不改变软件可观察行为的前提下,提高其可见性。降低修改成本

这是名词上的定义,

使用一系列重构手法 在不改变软件可观察行为的前提下,调整其结构

这是动词上的解释

这样听起来 重构就很像整理代码 它提供了一种更加高效可控的代码整理技术 ,也知道了如何使用才能将错误减少到最少。

我们重构的目的是使软件更容易被理解和修改。但必须对软件可观察的外部行为只造成很小变化,或不造成变化。这样与之对应的就是性能优化,因为性能优化很可能使得代码难以理解

在两个定义中都强调了这么一点

重构不会改变软件可观察的行为

也就是说 重构后的软件功能一如既往,任何用户,不论是程序员还是最终的客户,都不会感觉到有东西发生了更改

为什么需要重构

一个项目,如果一味的添加功能,只为短期利益,设计会逐渐的腐败崩坏,逐渐的偏离了设计之初的结构。这个时候程序员就很难通过阅读代码了解原本的逻辑。

而重构就非常像整理代码。所做的事情就是让原本的事情归位。代码结构的腐败是一个流失性的,越难看懂,就越难保护原本的设计,于是腐败的越来越快,经常的重构可以维持自己代码的结构形态。


在本书中列了很多重构的优点,这里大概说一下,

首先是可读性,重构后的代码可读性会变强,使得原本的代码更容易被理解,可读性非常重要,代码的后续读者对代码的运行的速度很可能没有那么的关心,但是他肯定对开发的速度非常的关心。

在大部分时候,我们写代码的时候,都不会考虑到第二个人,所以应该改变一下开发节奏,对代码做适当的修改,会使代码更好的被理解,

另外一方面 ,可以利用重构来理解不熟悉的代码,这个貌似有点玄学,我们拿到代码,为了理解用途,得反复的试,重构也可以做到这一点。尤其当这段代码没有人知道到底是个什么的时候。

第二点 可以帮你找到bug,这个感觉更玄学了,其实这是可读性增强了之后必然的一个优势。当你已经利用重构熟悉了代码,并且结构已经很好的时候,这个bug肯定是无所遁形的

其实无论优点,都可以归类为一个,重构可以帮助我们更快的开发,这个与直觉相反,多花了时间,怎么反而更快。因为良好的设计才是维持软件开发速度的根本。不然随着时间系统的腐败。一个新的功能迭代将会使得你非常疲惫。

何时重构

重构需要保持纯净。由本书两个帽子来比例,平时做的是添加新功能,那另外时间做的一件事情就是重构。添加新功能时不应该修改原有的代码,只管添加功能,通过测试。而重构时就不能添加新功能,只管改进程序结构。这个时候不需要什么测试,只在绝对必要的时候(比如接口变化)才会去修改测试。

其实在我们平时开发,就经常做这么一件事,首先写功能,后来就发现,把结构改变一下,整体的代码可读性以及维护会好很多,于是就用了重构的帽子,做了会工作。结构调整好了之后,又继续写,又发现这个结构不行了。于是又换了个帽子。继续优化结构。整体的进度是在一个渐进的过程。

再说几个重构的时机,一个是修复bug时重构,这个大家做的一般比较少,但是并不是没有必要,如果你不能一眼看出bug,显然是代码不够清晰,这就是重构的信号。

更多的时候是在代码review的时候进行重构。代码review是一种很好的东西,可以有助于传播知识,有经验的开发者可以利用这个时候把自己的经验传给相对欠缺的人,并且帮助不熟悉的人理解软件的更多细节。并且这个过程中,也可以让人提出更多有效的建议。

这得插一句 涉及极限编程中一个形式-‘结对编程’,把代码review发挥到极致,可以说边review边开发。完美了阐释了极限编程的这个名字语义。

间接层与重构

计算机科学是这样一门科学,它相信所有问题都可以通过增加一个间接层来解决

– Dennis DeBruler

间接层的价值非常大,搞得很多人都以为重构就是extract

但是这是一把双刃剑,把一个东西分为两份,就需要管理更多一份,如果对象委托一个对象,然后后者再委托给一个对象,代码就难以阅读。

间接层的好处非常多

  1. 允许逻辑共享
  2. 分开解释意图与实现
  3. 隔离变化
  4. 封装逻辑

关于间接层的价值 我推荐大家阅读代码大全第7章<高质量的子程序>

何时不该重构

我不是原教旨主义者,那么相信这个东西。很多时候你都不应该去重构,代码实在太混乱,重构它不如重写,重构反而会让你消耗更多的时间,这个决定做出来很困难,而且估计也没什么准则来帮助我们。只能说有那么个信号

现有代码不能正常运作,然后尝试去测试一下,发现全都是bug,根本不可能稳定下来。这个时候,不要考虑重构,放弃掉它

还有一个时候,项目已经到交付了,也要避免重构。

这本书有一个很好的例子,写代码就像借债,很多公司需要借债来帮助自己有效的运转,但是借债就有利息,过于复杂的代码造成的维护和扩展的成本就是利息,公司可以承担一定的利息,但是如果利息太高了就会被压垮。把债务管理好很重要,开发人员应该随时随地通过重构来偿还一部分的利息

这里插点想法,我发现很多人非常喜欢做非常严格的预先设计,用UML图或者更古老的CRC卡片,然后得到一个可被接受的解决方案,但是这个压力太大了,这意味着一个情况,如果将来对原始设计做任何修改,代价都将非常的高昂

如果你选择重构,问题的重点就可以转变了,你仍然需要做预先设计,但是不一定需要那么的正确,此刻你只需要得到一个足够合理的方案,问题的理解是随着你的进度而不断的加深的,你可能会察觉到最佳方案与你当初设想的有点差别,但是你有重构,不成问题,它会使得成本不再高昂

这就是一种思想的转变,软件设计可以简化了很多,不然,写一个需求,得考虑到很多后续的变化,让你心惊胆战。一方面你想建立一个足够灵活,足够牢靠的解决方案,承担所有变化,问题也在这里,这么一个灵活的系统,成本太高了,它肯定会比一个简单的系统要复杂很多,最终也会相对难以维护,虽然说的确是更加的灵活了,但是你知道,变化是可能存在系统各部分的,如果所有变化的地方都建立灵活性。复杂度非常的高,同时,这其中一些肯定用不上这么好的灵活性。但是你无法预测

有了重构,就有另外一条道了,当然仍然需要考虑到潜在的变化,但是你不必逐一实现它,应该问问自己,我写的这段代码,变化重构的难度大吗,如果很简单,就这样吧

代码的坏味道

以下开始抄书 具体了解的话还是得看书,我这里大概说下

  1. 重复代码

    这点应该没什么异议,逻辑的重复很可能造成维护的困难,同一份逻辑在两个地方存在,这本身就是一个问题,虽然它看起来很平稳的运行。

  2. 过长的函数

    这点在老项目中非常的常见,间接层的全部魅力都是在小函数中,一个方法或者说函数的代码长度一直是一个非常悠久的争议话题,到底多少才算长,

有兴趣的同学可以阅读代码大全第7章 这里拥有一些不同机构的报告,一个函数内行数对于代码可读性的影响

这里的话 基本手法大部分都是使用extract Method 这个手法,有个技巧让你来判断这个函数是否需要extract,就是看有没有注释,如果有,你或许可以考虑尝试抽离为一个新的方法 。

  1. 过大的类

如果一个类做了太多事情,一非常容易出现重复,二一起开发很容易冲突,三容易变的混乱。

这个时候可以利用类似一些Extract Class的手法,可以先考虑Extract Interface,然后再抽类

  1. 过长的参数列

一个函数如果它的参数非常的长,完全可以包成一个对象,再传递给它,这也免息对象编程的魅力,如果采用的充血模型开发,你或者可以考虑用实例对象来取代这个参数

  1. 发散式变化

    如果某个类经常因为不同的原因在不同的方向变化,比如一个类既可以修改数据库相关,也可以修改支付相关,那么这个类最好还是分成两个对象比较合适

针对某一个外界的变化导致的修改,都应该发生在同一个类中,而这个新类所有的内容都应该反应这个变化。为此,你可以找到某个特定原因造成的所有变化,然后用Extract Method提炼到一个新类中

6.霞弹式修改

这与上面的发散式变化非常的相像,但是他们恰恰相反,如果你遇到一个变化,需要改好多个不同的类的小修改,需要修改的代码散布四周。很难找,也很容易忘。这个时候就是将所有需要修改的地方放到一个类中。没有就创造一个。

7.依恋情节

依恋情节这么一个情况,它的气味是这样的,对某个类的兴趣要高于对自己的类的兴趣。听起来有点充血模型的感觉。比如一个车辆相关的类,但是它内部实现更关心的是运单相关的东西。那么这个类的函数应该被迁移掉,搬移到合适的地方去。

8.数据泥团

数据项会非常容易的成群结队在一起,非常容易见到,不同的函数相同的参数,不同的类相同的字段,这些东西其实可以拥有一个专属这个泥团的对象

9.基本类型偏执

这种东西就属于小对象,比如只有两三个int属性这种,专门给小任务用,但是其实这个小对象很有价值的,比如money类,这里面就两个属性币种以及数值,比如range类,一个起始值一个结束值,其实都是有价值的

10.switch语句

switch语句在面向对象编程中其实也是属于少用的一个东西,本质上说,switch的问题在于重复,经常容易发现switch语句散步在不同的地方。如果添加一个case,就得找到所有switch的地方去再添加一些。面向对象的多态可以很好的解决这个问题

11.夸夸其谈的未来性

这个东西就很有意思,也非常令人反感的一个东西。当一个人跟你说‘’我们以后总有一天会做这个事情的“然后搞了各种东西来处理这个没有的情况。这个代码的味道就很脏,很难闻。如果你确定它的确是函数都会被用到,那么可以,值得做,不然的话,还是删掉的好

12.狎昵关系

两个类联系太过紧密,甚至连private都想分享的时候,要不拆开,要不在一起。

13.中间人

中间人本来是好的,但是当你过度的运用委托,它就不好了。如果一个类,没有那么的必要,还是删掉这个层比较好。过多的层会造成很大的困扰的,不过这个问题比较少,多写代码的事情总是没人想干

14.过多的注释

注释好像很无辜,它不是很棒的吗,注释是这么一回事,一般过多的注释,都是代码写的太烂了,这是一种可以帮我们找到其他所有坏味道的一个好东西。

15.令人迷惑的暂时字段

很多时候,一个变量,只是为了一些特定的情况设的,但是这样的代价就是搞得代码不好被理解,你通常都认为这个对象所有时候都是需要这个字段的。这个东西没有被使用,而你又去猜,会让你非常的暴躁

给这个孤儿变量造个家,所有相关代码都放过去。

16.冗赘类

这得说一点,其实每个类它都是值钱的,它有身价的,以后会有人维护的,工作需要花钱的。它如果没有价值了,删了它。

17.平行的继承体系

继承的这么一个情况,属于霰弹式修改的特殊场景,如果你想增一个子类,而另外一个类也得增一个子类。你发现继承体系的类前缀与另外一个继承体系的类前缀完全相同,就是这么一种情况的坏味道

一般做法是 一个继承体系的实例引用另外一个继承体系的实例,然后再运用一些移动方法,字段的手法,就可以解决这个问题

18.过度耦合的消息链

一个对象到另外一个对象,然后后者再跳过去请求。只能这么说,导航结构很紧密。但是如果你把这个关系暴露到外面了。一旦对象间的关系发生任何修改,就会影响到客户端,然后也得修改

还是要将这个关系给它隐藏掉,或者最好还是找到消息链最后来看干什么,能不能抽出来,再推进去,不那么长。这种情况现在开发比较少见了

19.异曲同工的类

如果两个类做的事情非常的像,但是有不同的名字。移动一下的内部实现的方法的位置,或许你可以考虑下抽离出一个共有的父类。

20.不完美的类库

有的时候提供出来的类库不是那么的贴切我们的使用,这个时候我们也没法改他们的代码,只能在外面做点工作,比如封装一下

21.被拒绝的馈赠

这个也属于继承体系的一个问题,有的时候我们不想要父类的一些东西,只需要父类的一些东西,这个时候把继承改为委托会更好,或者可以尝试纯净父类的东西,将相关不需要的给它push down到子类

22.原始的数据类

这个估计见不到了,就是说这个类有一些字段,类似我们系统的中DO层,但是它没有get set这种基本的封装,直接访问,应该给它Encapsulate掉 不能这么直接

重构的一些不错的手法

这里只是抽各个章节的手法的一些粗略说明 对现在开发工作有用程度比较大的一些手法,一些有用层次低一点,但不能说没有用的我就不介绍了,最好还是看书

第六章 重新组织函数

Extract Method 提炼函数

这个不用多说,大家最常用的

将一些代码放到一个独立函数中,并让函数名称解释它的用途。

Inline Method 内联函数

这与上面是一样的,相反的一个手法,将函数内联进去,因为这段函数本体与名称同样的清晰易懂

Replace Temp with Query 以查询取代临时变量

在平时的业务代码开发中,比如说,有一段临时变量保存表达式的运算结果。我们就可以把这个表达式提炼到一个独立函数中,感觉像extract Method的变种。然后引用点给它替换到这个新函数上,这样,这个临时变量就被干掉了,而且通用了

Intorduce Explaining Variable 引入解释性变量

有一个很复杂的表达式,把结果给一个临时变量,用着个变量名来解释这个表达式的用途,比如我们经常用的Is前缀的字段。比如IsPay,IsNotify等等

Split Temporary Variable 分解临时变量

比如这一回事,一个临时变量,它的名字太泛化了,外部的函数又有足够多的东西,就很容易导致这个临时变量被赋值多次,这个时候就会导致混乱。应该分解这个变量,针对每次赋值的创造一个自己的临时变量。当然,说的并不是for循环的i变量,也不是fork-join中用来收集结果的result变量

Remove Assignments to Parameters (移除对参数的赋值)

代码对参数进行赋值 这种问题在我们代码中还是比较常见的,这种问题最大的毛病在于降低了清晰度,应该换成一个临时变量来做这个事情,因为入参本身就是自带一个语义,就是只读的语义,如果违法了这个约定的话,代码就会不清晰

Replace Method with Method Object 以函数对象取代函数

这个是因为有很多局部变量的情况下,无法extract method的情况下的一个手法。

将这个函数放进一个单独对象中,如此一来局部变量就成了对象中的字段,然后你可以在同一个对象中将这个大型函数分解为多个小型函数

第七章 在对象之间搬移特性

这个章节呢 是在对象之间搬移特性,, 决定把什么职责放到哪里是非常重要的一件事,但是又因为类常常出问题,然后臃肿不堪,这个时候就需要这个章节的手法了

首先

Move Method 搬移函数

这个是基本操作,一个类的函数与另外一个类交流更多,就应该被被移过去

Move Field 搬移字段

同上,不过是字段的维度,

Extract Class 提炼类

一个类做的事情比较多,太多了,就应该提炼出来其他的类。

建立一个新类,将相关的字段和函数从旧类迁移到新类中

Inline Class 内联类

这个用的也比较少,更多的时候我们都是在少写代码的道路一往无前

一个类做的事情太少了,将这个类的所有特性搬移到另一个类中,然后移出原类

Hide Delegate 隐藏委托关系

客户通过一个委托类调用另外一个对象,这个一下子可能无法立即理解,有些东西我们不是暴露这个关系的,比如说我们一线开发与具体的业务方,这个我们应该不能直接联系到。我们需要一个产品经理来做这么一层,隐藏掉这个委托关系

Remove Middle Man 移除中间人

这是与上面相反的一个手法,过犹不及。做了太多的简单委托,不如直接调用过去

第八章 重新组织数据

Encapsulate Field 自封装字段

就是比如public的字段给它封装为pojo,现在都是lombok这种东西直接简单一个注解解决,真是个好东西。

Replace Data Value with Object 以对象取代数据

有那么种数据,必须得跟其他数据在一起使用才有意义。这么种数据就封装成一个对象

Change value to Reference 将值对象改为引用对象

这个名字可能有点混肴,其实意思大概是说 从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象

比如这么一个多对多情况下希望变成一个一对多的列子,一个账单是有着对应的客户的,但是不能一个账单对应一个客户,是一个客户对应一批账单

Change Reference to Value 将引用对象改为值对象

这就是上面的回头路,其实所有重构手法都是要这样,重要的不是一往无前的决然,是想停就停的随意。一个人如果重构可以随时停掉,而不会有什么影响,那我觉得这个人的单论重构的水平已经是很优秀了

回到话题,为什么我们要重构回来,因为引用可能使得我们的关系变的错综复杂。而如果用原本的方式,这样的代码反而清晰明了

Replace Array with Object 以对象取代数组

这种重构手法是面临一种非常丑陋的情况的,比如我们在一些项目中,会看到用JSON来表示对象,对外进行承诺的。这种写法非常的丑陋。应该用对象替换掉它,在本书中,由于本书已经年代久远,Json可能还未出现。这里是用数组作列子的,不过意思都是相同的,因为人是很难记住这种信息的,说第一位是名字,第二位是年龄。必须得用字段名称以及相关的类名来描述它,这样的话,无须写注释,也无须死记

Replace Magic Number with Symbolic Constant 以常量取代魔法数

魔法数这个东西历史悠久啊,这种数字就是拥有特殊意义,但是又不能明确表示出来。这个时候,把它变成一个常量会好点。其实这个魔法数还有很多可能,比如是个类型码,可以考虑搞成枚举,或者说用类取代掉。

Encapsulate Collection 封装集合

对集合的把控其实也蛮重要,不过现在代码中大家都会下意识的习惯,并不会直接的去做一些很丑陋的写法,这也有赖于工程界的进步。这个手法的意思是说,一个函数原本是返回了一个集合,但是这个集合是属于这个类本身的,

这个写法比较少,我们现在都是Spring这种容器管理或者各种外部的持久化工具。

在这个情况下,应该只返回一个只读的副本出去,而不是直接提供出去,添加与删除的操作,需要在这个类本身单独做出两个方法处理。

Replace Subclass with Fields 以字段取代子类

这么一个情况是子类的差距太小了,只是一个字段上,这么一个情况就可以取消这些子类了,往上浮。

第九章 简化条件表达式

Decompose Conditional 分解条件表达式

当有一个复杂的条件语句,可以从if then elase 三个段落分别提炼出独立函数,然后函数名替换过去。这也这一章节可以说最重要的一个手法,虽然看起来很简单,但是有时候小改动就是能造成很好的效果

Consolidate Conditional Expression 合并条件表达式

有一系列的条件,都得到相同的结果。

将这些条件合并为一个,并这个抽出来作为一个独立函数

Consolidate Duplicate Conditional Fragments 合并重复的代码片段

这个是在每个条件分支上面都有这么一个函数,这样的话不如拿到外面来,减少这个重复代码

Remove Control Flag 移出控制标记

在传统的语言中,控制标记是比较常见的一种做法,用来判断条件检查停止的一个控制标记。但是既然又了break以及continue,以及现在java都可以支持break或者continue跳到指定的循环处。其实不需要这种写法了,这些写法可读性有点低。

Replace Nested Conditional with Guard Clauses 以卫语句取代嵌套条件表达式

条件逻辑过于复杂的时候很难看到正常的执行路线。这个时候就得用卫语句来表现所有的特殊情况。大家可能对这里卫语句这个名词了解的少点。

卫语句就是把复杂的条件表达式拆分成多个条件表达式,比如一个很复杂的表达式,嵌套了好几层的if - then-else语句,转换为多个if语句,实现它的逻辑,这多条的if语句就是卫语句.

Replace Conditional with Polymorphism 以多态取代条件表达式

手上有个条件表达式,根据对象的类型不同选择不同的行为。这就是经典的多态取代switch表达式的手法了。其实在面向对象的编程语言中,不像结构性编程语言,switch用的那么频繁,就是因为有多态这个神器。

做法大概就是将switch的每个分支放到子类的函数里,然后将原始的函数搞成抽象方法

当然在使用这个手法的前提,就是有一个继承体系。

Introduce Null Object 引入Null对象

这种手法有的人也把它叫做空对象设计模式,这是一种多态的好处,多态不用去管具体是什么类型,只管调用就好,其他的机制帮你搞定。所以说空对象就是为了这么一个情况的,它也可以正确的显示自己。这么做的话有个注意点,就是需要加入一个方法,isNull。或者说用一个nullable接口也不错,这样就可以删除掉那些判断是否null的地方了

第十章 简化函数调用

Rename Method 函数改名

非常推荐将复杂的处理过程拆解为小函数,但是如果做的不好,这就会使得费尽周折都理不清这些小函数的具体用途。避免这个问题的关键就在于起一个好名字。这个名字能准确的表达它的用途

起一个好名字并不容易,它需要丰富的经验。一个编程的高手,起名水平也非常的重要。有关这里我也要推荐代码大全这本书 第十一章变量名的力量

Add Parameter 添加参数

过长的参数列本身就是不好的味道,因为程序员很难处理这么长的参数,这个时候我们可以用一个对象来替代这些参数。

Remove Parameter 移出参数

很多时候多余的参数就是恶魔,而且以后可能还会用上它。不过一般这个情况下,你都可以通过多态来解决掉参数的传递问题。

Separate Query from Modifier 将查询函数与修改函数分离

某些对象返回对象状态值,又修改对象状态。建立两个不同的函数,其中一个负责查询,另一个负责修改

这是一种非常好的想法,明确出两种状态 一个是有副作用 一个是无副作用的。

如果你遇到这种方法,就应该拆成这么个模样

Parameterize Method 令函数携带参数

若干函数做了类似的工作,但在函数本体却包含了不同的值。

这种情况下就是因为一些小小不同的参数有一些不同。但是可以将这些各自分离的函数统一起来,通过参数来处理这些变化,用来简化问题。这样可以防止重复代码,提高灵活性

Replace Parameter with Explicit Methods 以明确函数取代参数

有一个函数,其中完全取决于函数值而采取不同行为

针对该参数的每一个可能,建立一个独立函数。

这跟上面的完全相反。这种适合给一个清晰的接口下。值得进行这项重构。

Preserve Whole Object 保持对象完整

从某个对象中取出若干值,作为某一次函数调用时的参数。

改为传递整个对象

为什么要这样做呢,有的时候变化就是这样,这次传一点点,后面很可能需要这个对象的其他参数,这个时候如果很难进行扩展,如涉及到其他的服务应用的情况下,就很困难。所以这个情况下传递整个对象比较合适,这还可以使得对象可读性更高。不过事情总是有两面性,有的时候这种手法会使得依赖结构恶化,那就不应该使用这个手法。

Replace Parameter with Methods 以函数取代参数

对象调用某个函数,并将所得结果作为参数,传递给另一个函数,而接受该参数的函数本身也能调用前一个函数

让函数接受者去除该项参数,并直接调用前一个函数

Introduce Parameter Object 引入参数对象

有一些参数总是不知不觉的就在一个方法变多了,这个时候就需要用一个对象替换这个了。

Remove Setting Method 移出设置函数

类的某个字段应该在对象创建时被设置,然后不再改变

这个指有些对象是不需要设置的,但是给出了一个set函数。这就不行了,过度的自由,就会被滥用。

Hide Method 隐藏函数

有一个函数,从来没人用过,可以注释掉,也可以直接private掉

Replace Constructor with Factory Method 以工厂函数取代构造函数

希望在创建对象时不仅仅是做简单的建构动作,

将构造函数替换为工厂函数

比如在需要根据类型码派生子类的时候

Replace Error Code With Exception 以异常替换为错误码

某个函数返回一个特点的代码,用以表示某种错误情况

改用异常

这跟上面我们说的有副作用和无副作用异曲同工。一种是错误码,一种是异常,这两种代表的其实是两个意思,一个是普通程序,一个是错误处理。代码的可理解性是一个非常重要的目标,相信我,这是比代码的复用是更高的目标要求。

Replace Exception with Test 以测试取代异常

异常不应该被滥用,这种情况本身就应该在测试阶段被拦截掉,而不应该抛出来一个异常。

修改这个调用者,在之前的条件判断的地方就拦截这个可能出现的错误。而不是让异常成为条件检查的替代品

第十一章 处理概括关系

这章有许多手法都是在处理类的概括关系 英文原文是generalization 即继承关系,这部分的手法我就不介绍了,因为在现状的开发环境中,各种工具推行的基本都是贫血模型的开发方式,在项目中更多的是通过包或者说名字来进行区分这个类的作用,而不是充血的完整的对象语义。对继承的情况使用的非常的少。当然也还是有很多值得学习的地方,限于篇幅所限,在此就不做过多介绍。只介绍几个比较常用的手法

Extract Interface 提炼接口

在两个类有部分接口相同的情况或者两个类的接口有部分相同

将相同的子集提炼到一个独立接口中

Form TemPlate Method 塑造模板函数

你有一些子类 其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同

将这些操作分别放进独立函数中,并保持它们都有的签名,于是原函数也就变得相同了,然后将原函数上移至超类

这其实也是在继承关系的一个手法,但是这个手法还是有意义的,这看起来很像一个模板方法模式。但其实又有所不同,不过这跟设计原则并不冲突,从另一个角度上说,这就是一个LSP原则,OCP原则,DIP原则都有些关系的好手法

Replace Inheritance With Delegation 以委托替代继承

某个子类只能使用超类接口中的一部分,或是根本不需要继承而来的数据

在子类中新建一个类以保持超类,调整子类,令它改为委托,现在的话就是IOC容器,一个依赖注入。然后去除掉两者之间的继承关系

其实每次说到这个手法的时候,都会把JDK中的stack与Vector类拿来鞭尸一下,虽然说现在已经没人用着两个类了,这就属于滥用的一种典型案例啊。有兴趣的可以具体看下

重构,复用与现实

虽然重构很美好,但是更多的时候你并不想重构你的程序,为什么呢

1.你不知道如何重构

2.你知道这是个长远的好东西,但是何必现在付出这个努力,说不定真的腐败的时候,你早就离职跳槽了

3.代码重构是一个额外的耗时的功能,老板付钱的时候,只是想让你写新功能

4.重构会对现有代码进行更改,可能会对现有代码造成破坏

但是随之时间的流逝,错误会不断的传播,复制,程序变的臃肿。当初的设计变的腐败不堪,修改的成本逐步上升

你或许也很同意Fred Brooks说的一句话

应对并处理变化,是软件开发的根本复杂性之一

重构就好像运动,吃合适的食物。都知道,需要锻炼身体,吃合适的东西才会有好身体,有些人不注重这些事情,而且目前看起来也没什么影响。所以可以依赖各种其他的招数,说服自己。但其实如果一直都忽视这些好习惯,其实都只是在欺骗自己。

重构另一方面非常让人关心的一点就是安全问题。

但其实对很多程序员来说,这些问题其实没那么的严重,我们总是跟孩子说,”安全第一”,但是我们却一直向往着血气方刚的驾驶员,酷炫的打斗。这其实比较讽刺,不给出自由,想让人飞,不现实。

一般的重构的安全性是定义是什么呢,一般来说是不会造成任何破坏的重构就是安全的。重构其实就是在不改变程序行为的情况修改程序结构,所以重构后与重构前应该完全相同。

怎么进行安全的重构,这应该是我们关注的重点,其实也没什么诀窍。

1 相信你自己

2 相信编译器

3 相信你的测试组件

4 相信review的力量

但其实你是会犯错的,编译器在一些微妙的场景下,无法帮助你,测试用例也是有限的,review人员很可能由于忙于自己的本质工作,没有办法的彻底检查。

幸运的是,对于重构安全性的检查往往是轻而易举的,例如,建立一个超类,就分为几个步骤,每个步骤都是比较简单的,比如创建函数,搬移字段。只要你确定你每一步都是安全的,那么整个复杂的重构也是安全的

在前面提到的现实世界问题,不仅仅存在于重构中,它们广泛存于在软件的演化和复用中。

复用的问题,与重构的问题也是非常的相似。

1.技术人员可能不知道 该复用什么,或者如何复用

2.技术人员可能对于采用复用方法缺乏动力,除非它们能获取到短期利益

3.如果要成功适应复用方法,开销,学习曲线和探索成本都必须要考虑

4.采用复用方法不该引起项目混乱,项目可能有很大的压力。尽管面对遗留系统的束缚,仍应让现有资产或实现发挥作用。新的实现应该与现有系统协同工作,或至少向下兼容。

这里有句话很不错

Geoffrey Moore把技术的发展接纳过程描述为一个曲线。前段包括先行者和早期接受者,中段急剧增加的是早期消费群体以及晚期消费群体,末端则是那些行动缓慢的人

如果一个技术或者思想想要获得成功,得到中段的人支持非常的重要,另一方面,许多先行者或早期接受者很有吸引力的想法,最后彻底失败,就是因为他们没能跨越鸿沟,让早期消费者与晚期消费者接纳他们。之所以有这样的鸿沟的原因在于,不同的人群有不同的消费动机。

前段的人 关注的是新技术,突破性的思想,范式的转移。早期以及晚期的消费群体主要关心的是成熟度,支持力度,以及这个产品在跟他们相似需求的其他人是否成功套用

研究人员往往是先行者,而软件主管或者一些软件开发者往往是早期或晚期消费者。如果你想让你的技术得人心,了解这一点差异非常重要。提倡一个新技术或者一种新的开发范式,得冒一些风险。得跟主管人员静心制定策略,与开发组协商,在研讨会与出版物向广大研究人员宣言这些技术的好处,在这个整个过程中。很重要的几件事:对员工进行培训,尽量获取到短期利益,减小开销。安全的引进新技术。这些知识,都是我从重构的研究中得来的

技术的传播是很困难,但不是做不到