`
sunjing21
  • 浏览: 157555 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
文章分类
社区版块
存档分类
最新评论

重构笔记

 
阅读更多

1.重构是在不改变软件可观察行为的前提下改善其内部结构.

2.不改变软件行为只是重构的最基本要求。要想真正让重构技术发挥威力,就必须做到 不需要了解软件行为 听起来很荒谬,但事实如此。如果一段代码能让你很容易了解其行为,说明它还不是那么迫切需要被重构。哪些最需要重构的代码,你只能看到其中的坏味道,接着选择对应的重构手法来消除这些坏味道,然后才可能理解它的行为。而这整个过程之所以可行,全赖你在脑子里记录着一份坏味道与重构手法对应表.

3.记住所有的坏味道,记住它们对应的重构手法,记住常见的重构方法,然后你才可能有信心面对各种复杂情况—学会所有招式,才可能无招胜有招。我知道这听起来很难,但我也知道这并不是你想象的那么难,你所需要的只是耐心,毅力和不断重读这本书

4.实际上,尽管我如此喜欢这本《重构》,但自从完成翻译之后,就再也没有读过它,不,不是因为我如此对它烂熟于心,而是因为重构已经变成我的另外一种生活方式,变成我每天的面包和黄油,变成我们整个团队的空气和水,以至于无需再到书中寻找任何神谕。而《设计模式》我倒是放在手边时常翻阅,因为总是记得不那么真切.

5.所谓重构是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序内部结构。重构是一种经千锤百炼形成的有条不许的程序整理方法,可以最大限度减少整理过程中引入的错误几率。本质上说,重构就是在代码写好之后改进它的设计.

6.你的态度也许倾向于尽量少修改程序:不管怎么说,它还运行的很好。你心里牢牢记住那句古老的工程谚语:如果它没有坏,就不要动它。这个程序也许还没有坏掉。但他造成了伤害。它让你的生活如此难过,因为你发现很难完成客户所需要修改。这时候,重构技术就该登场。

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

8.重构的第一步:第一步永远相同,我得为即将修改的代码建立一组可靠的测试环境。

9.要知道,代码块越小,代码的功能就更容易管理,代码处理和移动也就更轻松.

10.重构技术就是以微小的步伐修改程序。如果你犯下错误,就很容易便可以发现它.

11.更改变量名称是值得做的行为吗?绝对值得。好的代码应该清楚的表达出自己的功能,变量名称是代码清晰的关键。如果为了提高代码的清晰度,需要修改某些东西的名字,那么久大胆去做吧。

12.任何一个傻瓜都能写出计算机可以理解的程序,唯有写出人类可以理解的代码,才是优秀的程序员.

13.所有这些重构行为都使责任的分配更加合理,代码的维护更加轻松。重构后的程序风格,将迥异于过程化风格。

14.这些例子给我们最大的启发就是重构的节奏:测试,小修改,测试,小修改,测试,小修改。。。。。。正是这种节奏让重构得以快速而安全地前进.

15.何谓重构

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

<2>动词:使用一系列重构手法,在不改变软件软件可观察前提下,调整其结构。

16.重构的目的是使软件更容易的理解和修改.

17.强调的第二点是:重构不会改变软件可观察行为,重构之后软件功能一如既往。任何用户,不论最终用户或其他程序员,都不知道已经有东西发生变化.

18.两顶帽子 :重构和添加新功能.

添加新功能时,你不应该修改既有代码,只管添加功能。通过测试,你可以衡量自己的工作进度。重构时你就不能再添加功能,只管改进程序结构,你不应该添加任何测试(除非发现有先前遗漏的东西)。只在绝对必要时,在修改测试.

19 为何重构

<1>重构改进软件设计

如果消除重复代码,你就可以确定所有事件和行为在代码中只表述一次,这正是优秀设计的根本.

<2>重构使软件更容易理解

<3>重构帮助找到bug

<4>重构提高编程速度

20 何时重构

在我看来重构本来就不是一件应该特别拨出事件做的事情,重构应该随时随地进行

<1>三次法则

第一次做某件事情时只管去做;第二次做类似的事会产生反感,但是无论如何还是可以去做;第三次做类似的事,你就应该重构。

事不过三,三则重构

<2>添加功能时重构

<3>修改错误时重构.

<4>复审代码时重构

开始重构前我可以先阅读代码,得到一定程度的理解,并提出一些建议。一旦想到一些点子,我就会考虑是否可以通过重构立即轻松地实现它们,如果可以,我就会动手。这样做了几次以后,我就可以把代码看的更清楚,提出更多恰当的建议。我不必想象代码应该是什么样,我可以看见它是什么样。于是我可以获得更高层次的认识。如果不进行重构,我永远无法得到这样的认识 。

21 系统当下的行为,只是整个故事的一部分,如果没有认清这一点,你无法长期从事编程工作。如果你为完成今天的任务而不择手段,导致不可能在明天完成的任务,那么最终还是会失败.

22 是什么让程序如此难相与

<1>难以阅读的程序,难以修改

<2>逻辑重复的程序,难以修改

<3>添加新行为时需要修改已有代码的程序,难以修改

<4>带复杂条件逻辑的程序,难以修改

因此,我们希望程序:<1>容易阅读<2>所有逻辑都只在唯一地点指定

<2>新的修改不会危及现有行为<4>尽可能简单表达条件逻辑

23我的经验告诉我,对于快速创造软件,重构可带来巨大帮助。如果需要添加新功能,而原本设计却又使我无法方便地修改,我发现先重构在添加新功能会更快些。如果要修补错误,就得先理解软件的工作方式,而我发现重构是理解软件的最快方式。受进度驱动的经理要我尽可能快速完事,至于怎么完成,那就是我的事。我认为最快的方式就是重构,所以我就重构。

24间接层和重构

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

<2>间接层的价值

允许逻辑共享

分开解释意图和实现

隔离变化

封装条件逻辑

25重构的难题

<1>数据库

<2>修改接口

接口被修改,任何事情都可能发生

接口一旦发布,你就再也无法仅仅修改调用者而能够安全地修改接口,你需要一个更复杂的流程

简言之,如果重构手法改变已发布的接口,你必须同时维护新旧两个接口,直到所有用户都有时间对这个变化做出反应。幸运是,这不太困难。你通常都有办法把事情组织好,让旧接口继续工作。请尽量这么做:让旧接口调用新接口。当你修改某个函数名称时,请留下就函数,让它调用新函数。千万不要复制函数实现,那会让你陷入重复代码的泥潭中难以自拔。你应该使用java提供的deprecation设施,将旧接口标记为它。

还好我们有一个选择:不要发布接口

所以除非必要,不要发布接口。这可能意味需要改变你代码所有权观念,让每个人都可以修改别人的代码,以适应接口的改动。以结对编程的方式完成这一切通常是好主意。

26 何时不该重构

记住,重构之前,代码必须起码能够在大部分情况下正常运行

另外,如果项目已经接近最后期限,你也应该避免重构。如果项目已经非常接近最后期限,你不应该在分心于重构,因为已经没有时间重构。不过多个项目经验显示:重构的确能够提高生产力。如果最后你没有足够的时间重构,通常就表示你其实早就该重构

27重构与设计

如果你选择重构,问题的重点就转变。你依然做预先设计,但是不必一定找到正确接近方案。此刻的你只需要得到一个足够合理的解决方案就够啦。你很肯定地知道,在实现这个初始解决方案的时候,你对问题的理解会逐渐加深,你可能会察觉最佳解决方案和你当初设想有些不同。只要有重构这把利器在手,就不成问题。因为重构让日后的修改成本不在高昂

28 哪怕你完全了解系统,也请实际度量它的性能,不要猜测,猜测会让你学到一些东西,但十有八九你是错的。

29 重构和性能

除了对性能要求实时的系统,其他任何情况编写快速软件的秘密就是:首先写出可调软件,然后调整以求得足够速度

30重构起源

优秀程序员肯定至少会花一些实际清理自己的代码。这么做事因为,它们知道简洁的代码比杂乱无章的代码更容易修改,而且它们知道自己几乎无法一开始就写出简洁的代码

31你必须培养出自己的判断力,学会判断一个类内有多少实例变量算是太多,一个函数内有多少行代码才算太长。

32 重复代码 Duplicated Code

<1>同一个类的两个函数含有相同的表达式

<2>两个互为兄弟的子类内含有相同的表达式

<3>如果有些函数以不同的算法做相同的事,你可以选择其中较清晰的一个.

<4>两个毫不相干的类出现重复代码

33过长函数 Long Method

<1>拥有短函数的对象会活得比较好,比较长

<2>间接层所能带来的好处---解释能力,共享能力,选择能力—都是有小函数支持的

<3>如果你能给函数起个好名字,读者就可以通过名字了解函数的作用,根本不必去看其中写了什么

<4>我们遵守这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立的函数中,并以其用途(而非实现手法)命名

<5>关键不在于函数的长度,而在于函数做什么和如何做之间的语义距离

<6>如何确定该提炼那一段代码了?寻找注释.它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是提醒他你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只是一行代码,如果它需要以注释来说明,那也值得将它提炼到独立的函数

34 过大的类 Large Class

35 过长参数列 Long Parameter List

36 发散式变化 Divergent Change

<1>如果某个类经常因为不同的原因在不同的方向上发生变化,为此,你应该找出某特定原因而造成的所有变化,然后运用Extract Class将它们提炼到另一个类

37 Shotgun Surgery

<1>如果每遇到某种变化,都必须在许多不同的类做出许多小的修改

<2>发散式变化是指 一个类受多种变化影响,ShotgunSurgery 一种变化引发多个类相应的修改

38 依恋情结 Feature Envy

<1>函数对某个类兴趣高于对自己所处类的兴趣,这种通常是焦点便是数据

<2>我们原则:判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数摆在一起。

39 数据泥团 Data Clumps

40 基本类型偏执 Primitive Obsession

41 Switch惊悚现身 Switch Statements

42 平行继承体系 Parallel Inheritance hieraichies

43 冗余类 Lazy Class

44 夸夸其淡未来性 Speculative Generality

<1>如果所有装置都会被用到,那就值得那么做;如果用不到,就不值得做。用不上的装置只会挡你路,所以,把它搬开吧

45 令人迷惑的暂时字段 Temporary Field

<1>有时你会看到这样的对象:其内某个实例变量仅为某种特定情况而设,这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有变化。在变量未被使用的情况下猜测当初其设置目的,会让你疯狂。

46 过度耦合的消息链 Message Chains

47 中间人 Middle Man

48 InappropriateIntimacy

49 异曲同工得类

50 不完美的库存

51 纯稚的数据类 Data Class

52 被拒绝的遗赠 Refused Bequest

53 过多的注释 Comments

54 构筑测试体系

<1>如果你想进行重构,首要前提就是拥有一个可靠的测试环境

55 重新组织函数

<1>对过长函数,一项重要的重构手法就是Extract Method,它把一段代码从原先函数中提取出来,放进一个单独的函数.Inline Method正好相反:将一个函数调用动作替换为该函数体。

<2>提炼函数最大的困难就是处理局部变量,而临时变量则是其中一个主要的困难

<3>参数带来的问题比临时变量稍微少一点,前提是你不在函数内赋值给它们.

56 Extract Method 提炼函数

你有一段代码可以被组织在一起并独立出来

将这段代码放进一个独立函数中,并让函数名称解释该函数的用途.

<1>当我看见一个过长的函数或者一段需要注释才能让人理解用途的代码,我就会将这段代码放进一个独立的函数中。

<2>首先,如果每个函数的粒度都很小,那么函数被复用的机会就更大,其次,这会使高层函数读起来就像一系列注释;再次如果函数都是细粒度,那么函数的覆盖也会跟容易。

<3>只有当你能给小型函数很好地命名时,它们才能真正起作用,所以你需要在函数名称上下点功夫。

<4>在我看来,长度不是问题关键,关键在于函数名称和函数本体之间的语义距离.

<5>如果需要返回的变量不止一个,又该怎么处理。

有几种选择。最好的选择通常是:挑选另外一块代码来提炼。我比较喜欢让每个函数返回一个值,所以会安排多个函数,用以返回多个值。如果你使用的语言支持出餐数,可以使用它们带回多个回传值。但我还是尽可能选择单一返回值。

57 Inline Method 内联函数

一个函数的本地与名称同样清楚易懂

在函数调用点插入函数本体,然后移除该函数.

<1>另外一种需要使用Inline Method的情况是:你手中有一群组织不甚合理的函数。你可以将它们都内联到一个大函数中,再从中提炼出组织合理的小函数。

58 Inline Temp 内联临时变量

你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法.

将所有对该变量的引用动作,替换为对它赋值的那个表达式自身.

59 Replace Tempwith Query 以查询取代临时变量

你的程序以一个临时变量保存某一个表达式的运算结果.

将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。以后,新函数就可以被其他函数使用.

60 Introduce ExplainingVariable 引入解释性变量

你有一个复杂的表达式

将该复杂表达式(或其中一部分)的结果放到一个临时变量,以此变量名称来解释表达式用途.

<1>我比较喜欢使用Extract method,因为同一个对象中的任何部分,都可以根据自己的需要取用这些提炼出来的函数。一开始我会把这些新函数声明为private;如果其他对象也需要他们,我可以轻易释放这些函数的访问权限。

<2>那么应该在什么时候使用Introduce Explaining Variable 答案是:在Extract Method需要花费更大工作量时。如果我要处理的是一个拥有大量局部变量的算法,那么使用Extract Method绝非易事。这种情况下就会使用使用引入解释变量来清理代码,然后再考虑下一步该怎么办

61Split TemporaryVariable 分解临时变量

你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。

针对每次赋值,创建一个独立对应的临时变量

62 RemoveAssignments to Parameters 移除对参数的赋值

代码对一个参数进行赋值。

以一个临时变量取代该参数的位置

<1>在java重,不要对参数赋值;如果你看到手上的代码已经这样做了,请使用本重构手法

63Replace Methodwith Method Obect 以函数对象取代函数

你有一个大型函数,其中对局部变量的使用使你无法采用Extract Method

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

64SubstitueAlgorithm 替换算法

你想把某个算法替换为另外一个更清晰的算法

将函数本体替换为另外一个算法

65在对象之间搬移特性

<1>在对象设计过程中,决定把责任放到哪儿即使不是最重要的事,也是最重要的事之一。我使用面向对象技术已经十几年,但还是不能一开始就保证作对。这曾让我很烦恼,但现在我知道,在这种情况下,可以运用重构,改变原先的设计.

<2>类往往会因为承担过多责任而变得臃肿,这样情况下,我会使用Extract class将一个部分责任分离出去。如果一个类变得不太负责任,我就会使用Inline class将它融入另外一个类。如果一个类使用另外一个类。运用hide Delegate将这种关系隐藏起来。

66Move Method 搬移函数

你的程序中,有个函数与其所住类之外的另外一个类进行更多交流;调用后再,或者被后者调用.

在该函数最常引用的类中建立一个有着类似行为的新函数,将旧函数变成一个单独的委托函数,或是将旧函数完全移除.

67Move Field 搬移字段

你的程序中,某个字段被其所驻类之外的另外一个类更多地用到.

在目标类新建一个字段,修改源字段的所有用户,令他们改用新字段.

68Extract Class 提炼类

某个类做了应该有两个类做的事。

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

<1>你也许听过类似这样的教诲:一个类应该是一个清楚的抽象,处理一些明确的责任.

<2>另一个往往在开发后期出现的信号是类的子类化方式.如果你发现子类化只影响类的部分特性,或如果你发现某些特性需要以一种方式子类化,某些特性则需要以另外一种方式子类化,这就意味你需要分解原来类.

<3>使用Move Method将必要函数搬移到新类。先搬移较底层函数(也就是被其他函数调用 多于 调用其他函数 者),在搬移较高层函数.

69Inline Class 将类内联化

某个类没有做太多事情

将这个类的所有特性搬移到另外一个类中,然后移除原类

70Hide Delegate 隐藏委托关系

客户通过一个委托类来调用另外一个对象

在服务类上建立客户所需要的所有函数,用以隐藏委托关系.

如果某个客户先通过服务对象的这段得到另一个对象,然后调用后者的函数,那么客户就必须知晓这一层委托关系。万一委托关系发生变化,客户也晓得相应变化。你可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖。这么一来,即便将来发生委托关系的变化,变化也将被限制在服务对象中,不会涉及客户.

71Remove MiddleMan 移除中间人

某个类做了过多简单委托动作.

让客户直接调用受拖类

<1>我谈到了 封装受托对象 的好处,但是这层封装也是要付出代价的,它的代价就是:每当客户要使用受托类的心特性时,你就必须在服务端添加一个简单委托函数。随着受托类的特性越来越多,这一过程就会让你痛苦不已.

<2>重构的意思就在于:你永远不必说对不起------只要把出问题的地方修补好就行啦.

72IntroduceForeign Method 引入外加函数

你需要为提供服务的类增加一个函数,但你无法修改这个类

在客户类中建立一个函数,并以第一参数形式传入一个服务类实例

<1>如果你以外加函数实现一项功能,那就是一个明确信号:这个函数原本应该提供服务类中实现

<2>但是不要忘记:外加函数终归是权益之计。如果有可能,你仍然应该将这些函数搬移到它们的理想家园。如果由于代码所有权的原因使你无法做这样的搬移,就把外加函数交给服务类拥有者,请他帮你在服务类中实现这个函数.

73Introduct LocalExtension 引入本地扩展

你需要为服务类提供一些额外函数,但你无法修改这个类.

建立一个类,使它包含这些额外函数。让这个扩展成为源类的子类或包装类.

<1>如果只需要一两个函数,你可以使用Introduce Foreign Method。但如果你需要额外函数超过两个,外加函数就很那控制它们了.所以,你需要将这些函数组织在一起,放到一个恰当地方去。要达到这一目的,两种标准对象技术----子类化和包装

<2>所谓本地扩展是一个独立类,但也是被扩展类的子类型:它提供源类的一切特性,同时额外添加新特性。在任何使用源类的地方,你都可以使用本地扩展取而代之。

<3>使用本地扩展使你得以坚持 函数和数据应该被统一封装 的原则。如果你一直把本该放到扩展类中的代码零散地放置于其他类中,最终只会让其他这些类变得过分复杂,并使得其中函数难以被复用.

<4>在子类和包装类之间做出选择时,我通常首选子类.

74.重新组织数据

有时你确实需要访问函数,此时就可以使用Self Encapsulate Field得到他们,通常我会选择直接访问方式,因为我发现,只要我想做,任何时候进行这项重构都很简单

74 SelfEncapsulate Field 自封装字段

你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙.

为这个字段建立取值设置函数,并且只以这些函数来访问字段.

归根结底,间接访问变量的好处是,子类可以通过覆写一个函数而改变获取数据的途径;它还支持更灵活的数据管理方式,例如延迟加载

面临选择时,我会总是做两手准备。通常情况我会很乐意团队中的其他人意愿来做。我比较喜欢先使用直接访问方式,直到这种方式给我带来麻烦为止,此时我就会转而使用间接模式。

使用本项重构时,你必须小心对待在构造函数中使用设置函数的情况。一般说来,设置函数被认为应该在对象创建后再使用,所以初始化过程中的行为有可能与设置函数的行为不同。这种情况下,我也许在构造函数中直接诶方为字段,要不就是单独另建一个初始化函数.

75 Replace DataValue with Object 以对象取代数据值

你有一个数据项,需要与其他数据和行为一起使用才有意思

将数据项变成对象

76 Change Value toReference 将值对象改为引用对象

你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象

将这个值对象变成引用对象

在许多系统中,你都可以对对象做一个有用的分类:引用对象和值对象。前者就像客户,账户这样的东西,每个对象都代表真实世界中的一个实例。

77 ChangeReference To Value 将引用对象改为值对象

你有一个引用对象,很小且不可变,而且不易管理

将它变成一个值对象

78 Replace Arraywith Object

你有一个数组,其中的元素各自代表不同的东西.

以对象替换数组。对于数组重的每个元素,以一个字段表示

79DuplicateObserved Data 复制被监听数据

80ChangeUnidirectional Association to BiDirectional

将单向关联改为双向关联

两个类都需要使用对方特性,但其间只有一条单向连接

添加一个反向指针,并使修改函数能够同时更新两条连接.

81 ChangeBidirectional Association to Unidirectional

将双向关联改为单向关联

两个类之间有双向关联,但其中一个类如今不在需要另外一个类的特性

82 Replace MagicNumber with Symbolic Constant

以字面常量取代魔法数

你有一个字面数值,带有特殊含义

创建一个产量,根据其意义为它命名,并将上述的字面数值替换为这个常量

83 EncapsulateField 封装字段

你的类中存在一个public字段

将它声明为private,并提供相应的访问函数

84EncapsulateCollection 封装集合

有个函数返回一个集合

让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数

集合的处理方式应该和其他种类的数据有些不同。取值函数不该返回结合自身,因为这会让用户得以修改集合内容而集合拥有者却一无所知.

另外,不应该为这整个集合提供一个设置函数,但应该提供用以为集合添加、删除元素的函数。这样,集合拥有者就可以控制集合元素的添加和移除.

85 Replace Recordwith data class 以数据类取代记录

你需要面对传统变成环境中的记录结构.

为该记录创建一个哑数据对象`

86 Replace TypeCode with Class 以类取代类型码

类之中有个数值类型码,但它并不影响类的行为

以一个新的类替换该数值类型码

如果把那样的数值换成一个类,编译器就可以对这个类进行类型检验。只要为这个类提供工程函数,你就可以始终保证只有合法的实例才会被创建出来,而且它们都会被传递给正确的宿主对象

但是,在使用Repalce Type Code with Class之前,你应该先考虑类型码得其他替换方式,只有当类型码是纯粹数据时,也就是类型吗不会再switch语句中引其行为变化。你才能以类取代它.

87 Repalce TypeCode with subclasses 以子类取代类型码

你有一个不可变的类型码,它会影响类的行为

以子类取代这个类型码

如果你面对的类型码不会影响宿主类的行为,可以使用Repalce Type Code with Class来处理它们。但如果类型吗不影响宿主类的行为,那么最好的办法就是借助多态来处理变化行为

一般来说,这种情况的标志就是像Switch这样的表达式。这种条件表达式可能有两种表现形式:switch语句或者if-then-else结构。不论哪种形式,它们都是检查类型码值,并根据不同的值执行不同的动作。

为建立这样的继承体系,最简单的办法就是Repalce Type Code With Subclasses,以类型码得宿主类为基类,针对每种类型码建立相应的子类.

但是以下两种情况你不能那么做:<1>类型码在对象创建之后发生了改变<2>由于某些原因,类型码主类已经有子类。如果你恰好面临着两种情况之一,就不要使用Repalce Type Code with State/Strategy

Replace Type Code with subclasses的好处在于:它把对不同行为的了解从类用户哪儿转移到类自身。如果需要加入新的行为变化,只需要添加子类就行.

88Replace TypeCode with State/Strategy

你有一个类型码,它会影响类的行为,但你无法通过继承手法消去它

如果类型码得值在对象生命期中发生变化或其他原因使得宿主类不能被继承.

89 RepalceSubclass with Fields 以字段取代子类

你的各个子类的唯一差别只在返回常量数据的函数身上

修改这个函数,使它们返回超类中的某个新增字段,然后销毁子类.

90 简化条件表达式

较之于过程化程序而言,面向对象程序的条件表达式通常比较少,这是因为很多条件行为都被多态机制处理掉了。多态之所以更好,是因为调用者无需了解条件行为的细节,因此条件的扩张更为容易.

91 分解条件表达式 Decompose Conditional

你有一个复杂的条件 if-then-else语句

从if,then,else三个段落中分别提炼出独立函数

像这样的情况下,许多程序员都不会去提炼分支条件。因为这些分支条件往往非常短,看上去似乎没有提炼必要。但是,尽管这些条件往往很短,在代码意图和代码自身之间往往存在不小的差距。哪怕在上面这样一个小小的列子中,notSummar(date)这个语句也能够比原本的代码更好表达自身的用途。对于原来的代码。我必须看着它,想一想,才能说出其作用。当然,在这个简单的例子中,这并不是很难。不过,即使如此,提炼出来的函数可读性也更高一些—它看上去就像一段注释那样清楚而明白

92 ConsolidateConditional Expression 合并条件表达式

你有一系列条件测试,都得到相同结果

将这些测试合并为一个条件表达式,并将这些条件表达式提炼成为一个独立函数.

有时候你会发现这样一串条件检查:检查条件各不相同,最终行为却一致。如果发现这样情况,就应该使用 逻辑或 和 逻辑与将它们合并为一个条件表达式.

首先,合并后的条件代码会告诉你 实际上只有一次条件检查,只不过多个并列条件需要检查自己,从而使这一次检查用意更加清晰

条件语句的合并理由也同时指出不要合并的理由:如果你认为这些检查的确彼此独立,的确不应该被视为同一次检查,那么就不要使用本项重构.

93 ConsolidateDuplicate Conditional Fragments

和并重复的条件片段

在条件表达式的每个分支上有着相同的一段代码。

将这段重复代码搬移到条件表达式之外

94Remove ControlFlag 移除控制标记

在一系列布尔表达式中,某个变量带有控制标记的作用

以Break语句或者return语句取代控制标记

这就是编程语言提供Break语句和continue语句的原因:用它们跳出复杂的条件语句。去掉控制标记所产生的效果往往让你大吃一惊:条件语句真正的用途会清晰得多.

95 Replace NestedConditional with Guard Clauses

以卫语句取代嵌套的条件表达式

函数中的条件逻辑使人难以看清楚正常的执行路径.

使用卫语句表现所有特殊情况.

根据我的经验,条件表达式通常有两种表现形式。第一种形式是:所有分支都属于正常行为。第二种则是:条件表达式提供的答案中只有一种正常行为,其他都是不正常行为

如果两条分支都是正常行为,就应该使用if…else…的条件表达式:如果某个条件极其罕见,就应该单独检查条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常称为卫语句.

Replace Nested Conditional with Guard Clauses的精髓就是:给某一条分支以特别的重视。如果使用if—then—else的结构,你对if分支和else分支的重视是同等的。这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句就不同了,它告诉阅读者:这种情况很罕见,如果它真地发生了,请做一些必要的整理工作,然后退出.

在我看来,保持代码清晰才是最关键的:如果单一出口能使这个函数更加清楚易读,那么就使用单一出口:否则就不必这么做。

卫语句要不就从函数中返回,要不就抛出一个异常.

96 RepalceConditional with polymorphism

以多态取代条件表达式

你手上有个条件表达式,它根据对象类型的不同而选择不同的行为.

将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数.

多态最更本的好处就是:如果你需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显得条件表达式

97 引入Null对象Introduce Null Object

你需要再三检查某对象是否为null

将null值替换为null对象

多态的最根本好处在于:你不必要向对象询问 你是什么类型 而后根据得到的答案调用对象的某个行为------你只管调用该行为就是了,其他的一切多态机制会为你安排妥当.

请记住:空对象一定是常量,它们的任何成分都不会发生变化,因此women你可以使用Singleton模式来实现它,例如不管任何时候,只要你索求一个空对象,得到的一定是空对象的唯一实例

请记住:只有当大多数客户代码都要求空对象做出相同响应时,这样的行为搬移才有意义。注意,我说的是大多数而不是所有

你经常可以在表示数量的类中看到这样的特殊类。例如java浮点数有正无穷大和负无穷大和非数量等特列。特列类的价值是:它们可以降低你的错误处理开销,例如浮点运算决不会抛出异常。如果你对NAN做浮点运算,结果也会是NAN。这和空对象的访问函数通常返回另外一个空对象是一样的道理.

98 introduceAssertion

98 introduceAssertion引入断言

某一段代码需要对程序状态做出某种假设.

以断言明确表现这种假设.

常常会有这样一段代码:只有当某个条件为真时,该段代码才能正常运行。

这样的假设通常并没有在代码中明确表现出来,你必须阅读整个算法才看出。有时程序员会以注释写出这样的假设。而卧要介绍的是一种更好的技术:使用断言明确这些假设.

注意,不要乱用断言,请不要使用它来检查 你认为应该为真 的条件,请只使用它检查 一定必须为真 的条件.

但是,更多时间,断言的价值在于:帮助程序员理解代码正确运行的必要条件.

99 简化函数调用

多年来,我一直坚守一个很有价值的习惯:明确地将”修改对象状态”的函数和查询对象状态的函数分开设计

良好的接口只向用户展示不要展示的东西。如果一个接口暴露过多细节,你可以将不必要暴露的东西隐藏起来,从而改进接口的质量。毫无疑问,所有数据都应该隐藏起来(希望你不需要我来告诉你这一点),同时,所有可以隐藏的函数都应该被隐藏起来.

100 函数改名 Rename Method

函数的名称未能揭示函数的用途.

修改函数的名称

我极力提倡一种编程风格就是:将复杂的处理过程分解成小函数。但是,如果做得不好,这会使你费尽周折却弄不清楚这些小函数各自的用途。

给函数命名有一个好办法:首先考虑应该给这个函数写上一句怎样的注释,然后想办法将注释变成函数名称

生活就是如此。你常常无法第一次就给函数起一个好名称。这时候你可能会想:就这样将就吧,毕竟只是一个名字而已。当心!这是恶魔的召唤,是通向混乱之路,千万不要被它诱惑!如果你看到一个函数名称不能很好的表达它的用途,应该马上加以修改。记住,你的代码首先是为人写的,其次才是为计算机写的。而人需要良好的名称函数.

要想成为一个真正的变成高手,起名的水平至关重要.

101 Add Parameter 添加参数

将某个函数添加一个对象参数,让该对象带进函数所需要信息

实际上我比较需要说明的是:不使用本项重构的时机。除了添加参数外,你常常还有其他选择。只要可能,其他选择都比添加参数要好。因为他们不会添加参数列表的长度.

102 Remove Parameter 移除参数

函数本体不在需要某个参数

将参数去除

程序员可能进程添加参数,却往往不愿意去掉他们。他们打地如意算盘是,无论怎样,多余的参数不会引起任何问题,而且以后还可能用上它

参数代表函数所需要的信息,不同的参数数值有不同的意义。函数调用者必须为每一个参数操心该传什么东西进去。如果你不去掉多余参数,就是让你的每一位用户多费一份心

103Separate Query from Modifier

将查询函数和修改函数分离

某个函数既返回对象状态值,又修改对象状态.

建立两个不同的函数,其中一个负责查询,另外一个负责修改

任何有返回值的函数,都不应该看得到副作用。有些程序员甚至将此作为一条必须遵守的规则。就像对任何东西一样,我并不是绝对遵守它,不过我总是尽量遵守,而它也回报我很好的效果.

如果你遇到一个 既返回值有又副作用的函数,就应该试着将查询动作从修改动作重分割出来.

104Parameterize Method 令函数携带参数

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

建立单一函数,以参数表达哪些不同的值.

你可能会发现这样的两个函数:它们做着类似的工作,但因为少数几个值导致行为略有不同。这种情况下,你可以将各自分离的函数统一起来,并通过参数来处理哪些变化情况,用以简化问题。这样的修改可以去除重复代码,并提高灵活性,因为你可以用这个参数处理更多的变化情况.

105 Replace Parameter with explicit Methods

以明确函数取代参数

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

计算不考虑编译期检查的好处,只是诶了获得一个清晰的借口,也值得你执行本项重构。哪怕只是给一个内部的布尔变量值,相比之下,Switch.beOn也比Switch.setState(true)要清楚的多.

106 Preserve Whole Object 保持对象的完整

你从某个对象中取出若干值,将它们作为某一个词函数调用时的参数.

改为传递整个对象

如果被调用函数使用了来自另外一个对象的很多数据,这可能意味该函数实际上应该被定义在那些数据所属的对象中。所以,考虑Preserve Whole Obejct的同时,你应该考虑Move Method

107 Repalce Parameter with methods 以函数取代参数

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

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

如果函数可以通过其他途径获得参数值,那么它就不应该通过参数获得该值。过长的参数列表会增加程序阅读者理解难度,因此我们应该尽可能缩短参数长度.

108 Introduce Parameter Object 引入参数对象

某些参数总是很自然同时出现

以一个对象取代这些参数

我已经记不清楚多少次看到代码用某一个值表示一个范围,例如表示日期范围的start和end,表示数值范围的upper和lower,等等。我知道为什么会发生这样情况。毕竟我自己也经常这样做。不过,自从血洗Range模式之后,我就尽量以范围对象取而代之.

109 Remove SettingMethod 移除设置函数

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

去掉该字段的所有设置函数.

110 Hide Method 隐藏函数

有一个函数,从来没有被其他任何类用到

将这个函数修改为private

重构玩玩促使你修改函数的可见度。提高函数可见度的情况很容易想象;另一个类需要用到某个函数,因此你必须提高函数的可见度。

111Replace Constructorwith Factory Method

以工厂函数取代构造函数

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

将构成函数替换为工厂函数.

112 EncapsulateDowncast 封装向下转型

某个函数返回的对象,需要由函数调用者执行向下转型

将向下转型动作移到函数中.

113Replace ErrorCode with Exception

Code with Exception

以异常取代错误码

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

改用异常

如果程序崩溃代价很小,用户又足以宽容,那么就放心终止程序的运行就好啦。但如果你的程序比较重要,就需要以更认真的方式处理.

问题在于:程序中发现的错误的地方,并不一定知道如何处理错误。当一段子程序发现错误时,它需要让它的调用者知道这个错误,而调用者也可能将这个错误继续沿着链传递上去。许多程序都是用特殊输出表示错误。

Java有一种更好的错误处理方式:异常。这种方式之所以更好。因为它清楚地将普通程序和错误处理分开了,这使得程序更容易理解===我希望如今已经坚信:代码的可理解性应该是我们虔诚追求的目标.

决定是抛出受控异常还是非受控异常

<1>如果调用者有责任在调用前检查必要状态,就抛出非受控异常

<2>如果想抛出受控异常,你就可以新建一个异常类,也可以使用现有的异常类。

为了使这段代码使用异常,我首先需要决定使用是受控异常还是非受控异常。决定的关键在于:调用者是否有责任在取款之前检查余额,还是应该有withdraw()函数负责检查。如果检查余额是调用者得责任,那么取款金额大于存款金额就是一个编程错误。由于这是一个编程错误,所以我们应该使用非受控异常。另一方面,如果检查余额是withdraw()函数的责任,我就必须在函数接口中声明它可能抛出这个异常,那么也就提醒调用者注意这个异常,并采取措施.

114 ReplaceExeption with Test

以测试取代异常

修改调用者,使它在调用函数之前先做检查

异常只应该被用于异常的,罕见的行为,也就是哪些意料之外的错误行为,而不应该成为条件检查的替代品.

115处理概括关系

概括关系既继承关系

116 Pull Up Field 字段上移

连个子类拥有相同的字段

将该字段移动到超类

判断若干字段是否重复,唯一的办法就是观察函数如何使用它们,如果它们被使用的方式很相似,你就可以将它们归纳到超类去。

本项重构从两方面减少重复:首先它除去了重复的数据声明;其次它使你可以将使用该字段的行为从子类移动到超类,从而去除重复行为.

117 Pull Up Method函数上移

有些函数,在各个子类中产生完全相同的结果.

将该函数移动到父类

如果某个函数在各个子类中的函数体都相同。

有一种特殊情况也需要使用Pull Up Method:子类的函数覆盖了超类的函数,但却仍然做相同的工作。

118 Pull upConstructor Body

构造函数本体上移

你在各个子类中拥有一些构造函数,它们的本地几乎完全一样.

在超类中新建一个构造函数,并在子类的构造函数中调用它

119 Push DownMethod 函数下移

超类中的某个函数只与部分(而非全部)子类有关.

将这个函数移动到相关的那些子类去.

120Push Down Field字段下移

超类中的某个字段只被部分(而非全部)子类用到.

将这个字段移动到需要它的那些子类去.

121ExtractSubclass 提炼子类

类中的某些特性只被某些(而非全部)实例用到。

新建一个子类,将上面所说的那一部分特性移动到子类中。

122ExtractSuperclass 提炼超类

两个类有相似特性.

为这两个类建立一个超类,将相同特性移动到超类

重复代码时系统中最糟糕的东西之一。如果你在不同地方做同样一件事情,一旦需要修改那些动作,你就得平白做更多的修改.

123ExtractInterface 提炼接口

若干客户使用类接口中的同一子集,或者两个类的接口有部分相同.

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

类之间彼此互相作用的方式有若干种。使用一个类通常意味用到该类中的所有责任。另一种情况,某一组客户只使用类责任区中的一个特点子集。在一种情况则是,这个类需要与所有协助处理某特定请求的类合作.

对于后两种情况,将真正用到这部分责任分离出来通常很有意义.因为这样可以使系统的用法更清晰,同时也更容易看清系统的责任划分.

如果某个类在不同环境下扮演截然不同的角色,使用接口就是一个好主意。你可以针对每个角色以Extract Interface出相应的接口.另一种可以用上Extract Interface的情况是:你想要描述一个类的外部依赖接口(既这个类要求服务提供方提供的操作)。如果你打算将来加入其它种类的服务对象,只需要要求他们实现这个接口即可.

124CollapseHierarchy 折叠继承体系

超类和子类之间无太大区别.

将它们合为一体.

125Form TemPlateMethod 塑造模板函数

126Repalceinheritance with Delegation

以委托取代继承

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

在子类重新建一个字段用以保存超类;调整子类函数,令它改而委托超类;然后去掉两者之间的继承关系.

你常常会遇到这样的情况:一开始继承了一个类,随后发现超类中的许多操作并不真正适用于这个子类。这种情况下,你所拥有的接口并未真正反映出子类的功能。或者,你可能发现你从超类继承了一大堆子类并不需要的数据,或你可能发现超类中的某些protected函数对子类并没有什么意义.

你可以容忍,并接受传统说法:子类可以只使用超类功能的一部分。

如果以委托取代继承,你可以更清楚的表示:你只需要受托类的一部分功能。接口中的那一部分应该被使用,那一部分应该被忽略,完全由你主导控制.

127Replacedelegation with inheritance

以继承取代委托

你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托关系.

让委托类继承受托类.

两条告诫要记在心中。首先,如果你并没有使用受托类的所有函数,那么就不应该使用Repalce Delegation with inheritance,因为子类应该总是遵守超类的接口。如果过多的委托函数让你烦心,你有别的选择:你可以通过remove Middle Man让客户端调用受托函数,也可以使用Extract Superclass将两个类接口相同的部分提炼到超类中,然后让两个子类都继承这个新的超类,你还可以用类似的手法Extract Interface

128Tease ApartInheritance 梳理并分解继承体系

某个继承体系同时承担两项责任.

建立连个继承体系,并通过委托关系让其中一个可以调用另一个.

129ConvertProcedural Design to Objects

你手上有一些传统过程化风格的代码

将数据记录变成对象,将大块的行为分成小块,并将行为移到相关对象中.

130 ExtractHierarchy 提炼继承体系

你有某个类做了太多工作,其中一部分工作是以大量条件表达式完成的。

建立继承体系,以一个子类表示一种特殊情况.

当你遇到这种瑞士军刀般得类—不但能够开瓶开罐,砍小树枝,还能在演示会上打出激光强调重点=你就需要一个好策略。将它们各个功能梳理并分开.

总结:

这些技术如此精彩,可它们却仅仅是个开始,这是为什么?答案很简单:因为你还不知道何时使用它们,何时不应该使用;何时开始,何时停止;何时前进,何时等待。使重构能够成功,不是前面各自独立的技术,而是这种节奏。

你又是如何得知什么时候才真正懂得这一切?正是当你开始冷静下来的时候,对自己的重构技艺感到绝对自信—不论别人留下的代码多么杂乱无章,你都可以将它处理变好,好到足以进行后续开发—那是你就知道,自己已经得到。

不过,大多时候,得道德标志是:你可以自信地停止重构。在重构者得整个表演中,停止正是压轴大戏。一开始为自己选择一个大目标,例如:去掉一堆不必要的子类。然后你开始向着这个目标前进,每一步都走得小而坚定,每一步都有备份,保证能够回头,好的,你离目录愈来愈进,现在只剩下两个函数合并,然后就大功告成。

就在此时,意想不到的事情发现啦:你再也无法前进一步。也许是因为时间太短,你太疲倦;也许是因为一开始你的判断就出错,实际上不可能去掉所有子类;也许是因为没有足够的测试来支持你。总而言之,你的自信灰飞因灭,你无法再自信满满地胯下下一步。你认为自己应该没把任何东西搞乱,但也无法确定。

这是该停下来的时候了。如果代码以及比重构之前好,那么就把它集成到系统中,发布你的成果。如果代码并没有变好,就果断放弃这些无用的工作,回到起始点。

当你真正理解重构之后,系统的这个设计对你来说,就像源代码中的字符那样随心所欲操控。你可以直接感受整个设计,可以清楚看到如何将设计变得更灵活,也可以看到如何修改它;这里修改一点,于是这样表现.

应该如何学习?

<1>随时挑一个目标

某个地方的代码开始发臭了,你就应该将问题解决掉。你应该朝目标前进,达成目标后就停止。你之所以重构,不是为了探索真善美(至少不全是)而是为了让你的系统更容易被人理解,为了防止程序变乱.

<2>没有把握就停下来
<3>学习原路返回

<4>二重奏.


分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics