《重构》读书笔记

《重构》读书笔记

总览

第一部分

第一章从实例程序出发,展示设计的缺陷,对其重构可以了解重构的过程和方法。

第二部分

第二章讨论重构的一般性原则、定义和进行重构的原因。主要讲述概念性的东西。

第三部分

第三章介绍如何嗅出代码的“坏味道”以及如何用重构对其消除。

第四部分

第四章讲述构建java的测试环境

第五部分

第五章到第十二章介绍作者整理下来重构的方法。

第六部分

第十三章重构技术在商业化应用中出现的问题

第1章 重构,第一个案例1

1.1 起点1

如果发现需要为程序添加新特性,而代码结构使你无法很方便的达到目的,那就需要先进行重构,然后使新特性的添加容易进行,再添加新特性。

1.2 重构的第一步7

首先检查自己是否有一套可靠的测试机制。这些测试必须有自我检测的能力。因为重构有可能引入bug。

1.3 分解并重组statement()8

重构技术就是以 微小的步伐修改程序,如果发现错误,很容易可以发现它。
优秀的程序员应该写出人类容易理解的代码,而非仅仅是计算机能理解的代码。

  • 搬移金额计算代码(Move Method)
  • 去除不必要的临时变量(Replace Temp with Query)
  • 提炼常客积分计算(Move Method)
  • Move Method 如果一个方法运用目标对象的属性进行计算,那么请把这个方法抽象到目标对象的类中。

1.4 运用多态取代与价格相关的条件逻辑34

  • 最好不要在另一个对象的属性基础上运用switch语句。如果不得不使用,也应该在对象自己的数据上使用,而不是别人的数据上。
  • 当一个类需要运用多态的时候,但是假如其在生命周期内有可能变成另一个同胞类,那么就应该使用state设计模式来解决这个问题。

1.5 结语52

第2章 重构原则53

2.1 何谓重构53

  • 名词含义:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高理解性和降低修改成本。
  • 动词含义
    使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
  • 重构让软件更容易理解和修改
  • 重构不会改变软件的可观察行为,即使改变也只能是微小的影响,软件功能一如既往。
  • 两顶 帽子(时间分配)
    • 添加新功能:不应该修改已有代码,只关注新功能。增加新测试,通过测试衡量工作进度
    • 重构:只改变程序内部结构,不应该添加测试(存在遗漏),不修改测试(除非接口发生变化)
    • 软件开发在这两者之间切换

2.2 为何重构55

  • 改进软件设计:

    • 程序的设计在没有重构的情况下逐渐腐败变质,功能的增加或者修改可能使代码越来越难以理解,就越难保护其中的设计
    • 消除重复的代码一方面是程序运行更快,一方面是方便未来的修改,只用在一处修改即可不用修改多处。
  • 软件更容易理解:

    • 及时填补“想要它做什么”和“告诉它做什么”之间的缝隙。重构的核心就是要“准确说出我所要的”
    • 重新阅读代码的人有可能是自己,他人。
    • 通过重构可以把不熟悉的代码的用途理一遍,加深对代码的理解
  • 帮助找出bug:这个是建立在代码容易理解之上的

  • 提高编程速度:重构达到良好的设计,而良好的设计更容易修改,增加功能,调试。

2.3 何时重构57

  • 三次法则:第一次的时候做某事尽管去做。第二次的时候对它产生反感,还是继续去做。第三次再做类似的时候,就应该重构了。
  • 添加功能时重构:一方面可能是需要理解需要修改的代码,另一方面是使增加新特性更加容易。
  • 修补错误时重构:出现bug的时候,难以找出问题所在的时候,很有可能是代码不清晰导致查找bug的困难。
  • 复审代码时重构:
    • 复审代码有助于知识的传播,有利于代码被编写者之外的人理解。
    • 重构是有利于增强复审代码的能力,重构需要先阅读代码得到一定程度的理解,得到一些建议,然后动手实现。所以重构有利于知道合理的代码应当是怎么样的。
    • 复审团队需要精炼,就 一个审查者和一个原作者。较大的项目可以通过 UML图去展示代码的逻辑。
  • 程序难以相与的原因:
    • 难以阅读的程序,难以修改
    • 逻辑重复的程序,难以修改
    • 添加新特性需要修改已有代码的程序,难以修改
    • 带复杂逻辑判断的程序,难以修改
  • 对应的期望:
    • 容易阅读
    • 所有逻辑都只有唯一地点指定
    • 新的改动不会危及现有行为
    • 尽可能简单表达逻辑

2.4 怎么对经理说60

  • 不要告诉经理:经理是进度驱动,就是要求开发者尽快完成任务。而对于我来说最快完成任务的方式就是先重构。
  • 很多时候重构都为程序引入间接层。把大型对象拆分成小对象,把大型函数拆分为小型函数。
    • 允许逻辑共享:一个函数在不同地点被调用。子类共享超类的方法。
    • 分开解释意图和实现:通过类名和函数名解释自己的意图
    • 隔离变化:在不同地方使用同一个对象,需要修改一处逻辑,那么可以做出子类,并在需要的时候修改这个子类。
    • 封装条件逻辑:运用多态。将条件逻辑转化为消息模式。
  • 减少间接层:当间接层只在一处使用,那么需要将其消除。

2.5 重构的难题62

  • 数据库:
    • 程序与数据库耦合在一起。另一方面是数据迁移,是向繁琐的事项。
    • 在非关系型数据库,可以在数据库和对象模型中插入一个分离层,隔离两者之间的变化
  • 修改接口
    • 对于已经发布的接口需要可能需要维护旧接口和新接口,用deprecated修饰旧接口。
    • 不发布新接口,在旧接口中调用新接口。
    • 假如新接口抛出编译时异常,那么可以在旧接口中调用新接口并将编译时异常转化为运行时异常。
  • 何时不重构
    • 重构之前,代码必须能够在大部分情况下 正常运行,不然就不应该重构,而应该是 重写
    • 到了Deadline,应该避免重构。

2.6 重构与设计66

  • 重构与设计是彼此互补的。
  • 预先设计是必须,预先设计不可能做到完全正确,随着对问题的逐渐深入,通过重构可以改善程序的质量。
  • 重构减轻了设计的难度和压力,在程序不断修改的过程逐步完善程序的设计。

2.7 重构与性能69

  • 重构是有可能导致程序运行变慢的。
  • 除了对实时有严格要求的程序,编写快速软件的秘诀是:首先写出可调的程序,然后调整它以达到足够的速度
  • 经过分析大部分程序的大半部分时间是运行在一小半代码上,所以对所有代码一视同仁是错误的。
  • 性能优化放在开发的后期,通过分析工具找出消耗大量时间空间的地方,然后集中精力优化这些地方。

2.8 重构起源何处71

第3章 代码的坏味道75

3.1 DuplicatedCode(重复代码)76

  • 同个类两个函数存在相同表达式:ExtractMethod(提炼函数)
  • 互为兄弟类内存在相同表达式:
    • ExtractMethod-》PullUpMethod(函数上移)
    • 如果代码只是相似:先运用ExtractMethod(提炼函数)分开再Form TemPlate Method(塑造模板函数)
  • 两个毫不相干的类存在重复代码:ExtractClass(提炼类)

3.2 LongMethod(过长函数)76

  • 原则:每当感觉需要以注释来说明什么的时候,我就将需要说明的代码放到一个独立的函数里面
  • 只要函数名称能够 解释用途,我们就应该毫不犹豫地做。
  • 关键不在函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。
  • 具体情况
    • 函数有大量参数和临时变量:ExtractMethod(提炼函数)
    • 用ReplaceTempwithQuery(以查询取代临时变量)消除临时变量
    • 用IntroduceParameterObject(引入参数对象)或者PreserveWholeObject(保持对象完整)来将多长的参数列表变得简洁一点。
    • 如果按照上述步骤还存在太多变量和参数就需要用到ReplaceMethodwithMethodObject(以函数对象取代函数)
    • 条件表达式可以用DecomposeConditional(分解条件表达式)解决
    • 可以将循环内的代码提炼为函数。

3.3 LargeClass(过大的类)78

  • 有时候类并非在所有时刻都使用实例变量:使用ExtractMethod和ExtractSubclass(提炼子类)
  • 类中有太多代码:ExtractClass(提炼类)ExtractSubclass(提炼子类),甚至可以使用提炼接口的方式分解类的行为。
  • 存在GUI的时候,可以DuplicateObservedData(复制“被监视数据”),分离数据和行为到领域模型中去。

3.4 LongParameterList(过长参数列)78

  • 如果可以调用已有对象获取的话可以使用ReplaceParameterwithMethods(以函数取代参数)
  • 将来自同一对象的数据收集起来,以该对象替代:PreserveWholeObject(保持对象完整)
  • 如果几个参数总是同时出现,那么可以考虑IntroduceParameterObject(引入参数对象)

3.5 DivergentChange(发散式变化)79

  • 一个类受多种变化影响:加上一个功能需要修改类中多个函数
  • 目标是每个对象都可以只因一种变化而需要修改
  • 方法:可以将提炼类来达到。

3.6 ShotgunSurgery(霰弹式修改)80

  • 遇到某种变化,需要在许多不同类做小修改。
  • 可以通过移动函数、移动字段、内联类把一种变化一系列变化放到同一个类中。
  • 对比:DivergentChange(发散式变化)是一个类受多个变化影响;ShotgunSurgery(霰弹式修改)是一个变化引起多个类相应修改。

3.7 FeatureEnvy(依恋情结)80

  • 函数对某个类的兴趣高过对自己类的兴趣
  • 通过移动函数放到该合适的位置。

3.8 DataClumps(数据泥团)81

  • 数据项总是成群结队出现
  • 判断方法:删除众多数据项的一项,这么做其他数据是否失去意义。如果不再有意义就需要提炼为参数对象。

3.9 PrimitiveObsession(基本类型偏执)81

  • 有些字段可以用对象表示更准确ReplaceDataValuewithObject(以对象取代数据值)
  • 对于不影响行为的类型码可以ReplaceTypeCodewithClass(以类取代类型码)
  • 影响行为的类型码可以ReplaceTypeCodewithSubclasses(以子类取代类型码),类型码在运行时会变化就用ReplaceTypeCodewithState/Strategy(以State/Strategy取代类型码)

3.10 SwitchStatements(switch惊悚现身)82

  • 使用ReplaceTypeCodewithSubclasses(以子类取代类型码)或者ReplaceTypeCodewithState/Strategy(以State/Strategy取代类型码)
  • 轻量级的解决方法:ReplaceParameterwithExplicitMethods(以明确函数取代参数)

3.11 ParallelInheritanceHierarchies(平行继承体系)83

  • 每当为一个类增加子类必须也为另外一个类增加一个子类
  • 策略是让一个继承体系的实例引用另一个继承体系的实例。

3.12 LazyClass(冗赘类)83

  • 内联类或者Collapse Hierarchy(折叠继承体系)来解决

3.13 SpeculativeGenerality(夸夸其谈未来性)83

  • 内联类或者Collapse Hierarchy(折叠继承体系)来解决
  • 函数参数没被用上RemoveParameter(移除参数)
  • 函数名称过于抽象RenameMethod(函数改名)

3.14 TemporaryField(令人迷惑的暂时字段)84

  • 对象中某个字段仅为特定情况而设。
  • 提炼类来解决

3.15 MessageChains(过度耦合的消息链)84

  • 获取一个对象,再通过该对象获取另外一个对象进行操作:HideDelegate(隐藏“委托关系”)

3.16 MiddleMan(中间人)85

  • 过度委托形成中间人:RemoveMiddleMan(移除中间人)
  • 如果中间人还有其他行为,Replace Delegation with Inherited(以继承取代委托)

3.17 InappropriateIntimacy(狎昵关系)85

  • 两个类过于亲密,花费太多时间去探究彼此private成分
  • 移动字段和移动方法减少狎昵
  • ChangeBidirectionalAssociationtoUnidirectional(将双向关联改为单向关联)
  • 如果两个类实在情投意合:可以使用ExtractClass(提炼类),让他们使用新类进行交互。

3.18 AlternativeClasseswithDifferentInterfaces(异曲同工的类)85

  • 两个函数做了相同的事情却有不同的签名

3.19 IncompleteLibraryClass(不完美的库类)86

  • 库函数不够好,需要加入一些操作,其实类似于 适配IntroduceForeignMethod(引入外加函数)
  • 如果需要加入大量的操作,IntroduceLocalExtension(引入本地扩展)

3.20 DataClass(纯稚的数据类)86

  • 类只有数据没有行为,其他类存在对该类的数据进行取值设值操作
  • 有public字段:EncapsulateField(封装字段)
  • 对于不该被其他类修改的字段:RemoveSettingMethod(移除设值函数)

3.21 RefusedBequest(被拒绝的遗赠)87

  • 如果类不想得到另一个类全部东西,只对部分感兴趣。
  • 可以使用Replace inherited with Delegation(以委托取代继承)来处理

3.22 Comments(过多的注释)87

  • 试试提炼方法来解决注释过多问题

第4章 构筑测试体系89

4.1 自测试代码的价值89

4.2 JUnit测试框架91

4.3 添加更多测试97

第5章 重构列表103

5.1 重构的记录格式103

5.2 寻找引用点105

5.3 这些重构手法有多成熟106

第6章 重新组织函数109

6.1 ExtractMethod(提炼函数)110

  • 无局部变量:直接抽取方法
  • 含有局部变量
    • 局部变量只在提炼代码块内被读取值:将局部变量作为方法参数
    • 局部变量在提炼代码块内被赋值:1只在提炼代码内被使用->将局部变量提炼到新该方法内;2在提炼代码块后->使用就返回局部变量修改后的值

6.2 InlineMethod(内联函数)117

  • 当函数的名称与其本体都一眼清晰明了,在函数调用点插入函数本体,移除该函数。
  • 有一群不甚合理的函数,可以先内联到大型函数然后再提炼出合理的小函数

6.3 InlineTemp(内联临时变量)119

  • 当临时变量只是被一个简单表达式赋值一次,而它妨碍其他重构方法
  • 方法:将所有对该变量的引用动作替代成对它赋值的表达式本身。
  • 情形:
    • InlineTemp多半是为ReplaceTempwithQuery(以查询取代临时变量)准备
    • 临时变量被一次赋值后,临时变量作为函数的返回值。

6.4 ReplaceTempwithQuery(以查询取代临时变量)120

  • 情况:你的程序以一个临时变量保存一个表达式的计算结果
  • 做法:将表达式提炼出独立的函数,然后临时变量的调用替换成新函数的调用。此后新函数也能被调用。
  • 具体做法:
    • 将提炼出来的函数用private修饰
    • 如果独立函数有副作用,那对它进行SeparateQueryfromModifier(将查询函数和修改函数分离)

6.5 IntroduceExplainingVariable(引入解释性变量)124

  • 将复杂表达式的结果赋值给一个临时变量,用临时变量名称来解释表达式的用途

6.6 SplitTemporaryVariable(分解临时变量)128

  • 临时变量被赋值超过一次,但是既不是 循环变量也不是被用于 收集计算结果
  • 原因:一个变量应该承担一个责任,如果被赋值多次很可能承担了多个责任
  • 做法:针对每次赋值,创建新的临时变量

6.7 RemoveAssignmentstoParameters(移除对参数的赋值)131

  • java是值传递,对参数的任何修改都不会再调用端造成影响,所以对于 用过引用传递的人可能会发生理解错误
  • 参数应该仅表示“被传递过来的东西”

6.8 ReplaceMethodwithMethodObject(以函数对象取代函数)135

  • 情形:在大型函数内,对局部变量的使用导致难以使用ExtractMethod(提炼函数)进行重构
  • 做法:将这个函数放入一个对象里,局部变量变成对象成员变量,然后可以在同一对象中将这个大型函数分解为多个小型函数。
  • 原因:局部变量会增加分解函数的困难度

6.9 SubstituteAlgorithm(替换算法)139

  • 把某个算法替换成更清晰的做法(算法)(有点废话)。

第7章 在对象之间搬移特性141

7.1 MoveMethod(搬移函数)142

  • 情形:程序中有个函数与所驻类之外的另一个类进行更多交流,调用后者或者后者调用该函数
  • 做法:在该函数最常引用的类中定义相似行为的新接口,将旧函数变成委托函数或者将旧函数删除。
  • 具体做法:
    • 检查源类中被源函数使用的一切特性,如果特性被其他函数使用,考虑这些函数一起搬移
    • 检查源类的子类和超类,看看是否有该函数的声明,如果出现,很可能不能搬移。
    • 目标类需要使用源类的特性:1将该特性转移到目标类;2建立目标类到源类之间引用。3将源类作为参数传给目标类4将该特性作为参数传给目标类
    • 如果源函数包含 异常处理,需要考虑是在目标类还是源函数处理

7.2 MoveField(搬移字段)146

  • 情形:程序中有个字段与所驻类之外被另一个类使用(包括设置取值函数的间接调用),后者调用该字段
  • 做法:将该字段搬移到目标类
  • 具体做法:建立从“旧类访问新类”的连接关系,除非真正需要 不要建立从“新类到旧类”的关系

7.3 ExtractClass(提炼类)149

  • 情形:一个类做了两个类的事
  • 做法:建立新类,将相应的字段和函数放到新类

7.4 InlineClass(将类内联化)154

  • 情形:某个类没做太多的事情,与ExtractClass(提炼类)相反
  • 做法:将这个类的所有特性搬移到另一类中,移除该类。
  • 判断依据:当一个类不再承担足够责任

7.5 HideDelegate(隐藏“委托关系”)157

  • 情形:客户端通过委托类来调用另一个对象
  • 做法:在服务类上建立客户端所需的函数,然后隐藏委托关系
  • 依据:符合“封装”的特性。当委托类发生变化不会对客户端造成影响

7.6 RemoveMiddleMan(移除中间人)160

  • 情形:某个类做了过多的委托动作
  • 做法:让客户端直接调用委托类
  • 依据:当原委托类的特性越来越多,服务类的委托函数将越来越长,需要让客户端直接调用,避免服务类沦为中间人。

7.7 IntroduceForeignMethod(引入外加函数)162

  • 情形:需要为服务类某个函数增加功能,但是不能修改该类
  • 做法:新建函数并将服务类的对象实例作为参数传入。
  • 具体情形:如果需要为服务类增加 大量的方法,请考虑使用IntroduceLocalExtension(引入本地扩展)

7.8 IntroduceLocalExtension(引入本地扩展)164

  • 情形:需要为服务类某个函数增加函数,但是不能修改该类
  • 做法:建立新类,使它包括这些额外函数,让这个扩展类作为服务类的子类或者包装类。
  • 具体情况:如果需要对数据进行修改要波及服务类对象,那么使用包装类的方式。如果不需要,使用子类化的方式

第8章 重新组织数据169

8.1 SelfEncapsulateField(自封装字段)171

  • 情形:直接访问一个字段,但是字段之间的耦合关系逐渐变得笨拙。
  • 做法:自封装就是在对于类内部的字段也封装一个设值取值的函数。
  • 争论:字段访问方式是直接访问还是间接访问一致争论不断
  • 间接访问的好处
    • 修改获取数据的途径;
    • 支持更灵活的数据管理;如延迟加载(需要用到才加载)等。
  • 直接访问的好处
    • 容易阅读代码,不会需要转换一下这个函数是取值函数。

8.2 ReplaceDataValuewithObject(以对象取代数据值)175

  • 情形:假如一个数据项需要与其他数据一起使用才有意义。
  • 做法:将数据变成对象。

8.3 ChangeValuetoReference(将值对象改为引用对象)179

  • 情形:从一个类衍生出彼此相似的对象的实例,希望把它们替换为同一个对象,ps:方便统一修改
  • 做法:将值对象变成引用对象
  • 区别:
    • 引用对象每个都对应现实中一个对象(==)
    • 值对象只关心其值是否相等。(重写equals()和hashcode()方法)
  • 具体做法:
    • 需要使用工厂模式来创建对象
    • 需要一个类(或者是自身)用字典或者静态表来保存对象
    • 决定对象是预先创建还是动态创建

8.4 ChangeReferencetoValue(将引用对象改为值对象)183

  • 情形:有一个引用对象且 很小(创建太多值对象内存消耗大) 不可变(无需修改对象),那么应该将其转换为值对象
  • 具体做法:
    • 查看是否是不可变对象或者可修改成不可变对象
    • 重写hashCode和equals()方法
    • 取消使用工厂模式和将对象的构造函数设为public

8.5 ReplaceArraywithObject(以对象取代数组)186

  • 情形:如果数据存储了不相似的数据,元素代表不同的东西。
  • 做法:将数组变成对象,数组的每个元素用字段表示

8.6 DuplicateObservedData(复制“被监视数据”)189

  • 情形: 有领域数据置身于GUI控件中,而领域函数需要访问这些数据
  • 做法:将该数据复制到领域模型中。建立Observer模式,同步UI和领域模型的数据。

8.7 ChangeUnidirectionalAssociationtoBidirectional(将单向关联改为双向关联)197

  • 情形:被引用类需要得到引用类做一些处理
  • 具体做法:
    • 两者是一对多关系,有单一引用承担控制关联关系责任
    • 如果某个对象(Task)是另一个对象(Project)的组件,由后者负责控制。
    • 如果两者之间都是多对多关系,那么由谁负责都没关系

8.8 ChangeBidirectionalAssociationtoUnidirectional(将双向关联改为单向关联)200

  • 情形:两个类有双向关联,但是一个类不关心另一个类的特性
  • 做法:去除双向关联
  • 原因:
    • 双向关联可能造成僵尸对象,不能被清除释放内存。
    • 使两个类存在耦合关系,一个类的变化会导致另一类的变化。

8.9 ReplaceMagicNumberwithSymbolicConstant(以字面常量取代魔法数)204

  • 情形:有一个字面常量(除了0和1之外)
  • 做法:创建常量赋值以该字面常量,给予命名。

8.10 EncapsulateField(封装字段)206

  • 情形:一个类有public字段
  • 将它声明为private,并提供相应的访问函数

8.11 EncapsulateCollection(封装集合)208

  • 情形:有函数返回集合
  • 做法:让该函数返回只读副本,并在该类提供增加和删除集合元素的函数
  • 具体做法:不应该提供集合的设值函数

8.12 ReplaceRecordwithDataClass(以数据类取代记录)217

  • 情形:面对传统编程环境的记录数据
  • 做法:为该记录创建一个“哑”数据对象。

8.13 ReplaceTypeCodewithClass(以类取代类型码)218

  • 情形:类中有个数值型类型码,不影响类的行为
  • 做法:以一个新类替代类型码

8.14 ReplaceTypeCodewithSubclasses(以子类取代类型码)223

  • 情形:有一个不可变的类型码且影响类的行为
  • 做法:以子类取代这个类型码

8.15 ReplaceTypeCodewithState/Strategy(以State/Strategy取代类型码)227

  • 情形:有一个类型码且影响类的行为,但是无法通过继承消除(类型码可变化)
  • 做法:以状态对象取代。

8.16 ReplaceSubclasswithFields(以字段取代子类)232

  • 情形:各个子类唯一区别只在“返回常量的数据”的函数上
  • 做法:修改这些函数使它们返回超类的某个(新增)字段,然后销毁子类。

第9章 简化条件表达式237

9.1 DecomposeConditional(分解条件表达式)238

  • 情形:if-then-else语句,不同分支做不同事情形成大型函数,本身就难以阅读,尤其在带有复杂条件的逻辑中。
  • 做法:
    • 将if语句提炼为函数
    • 将then和else段落提炼为函数
    • 存在 嵌套,先判断是否可以用ReplaceNestedConditionalwithGuardClauses(以卫语句取代嵌套条件表达式)消除。不行再分解每个条件

9.2 ConsolidateConditionalExpression(合并条件表达式)240

  • 情形:有一系列条件判断都返回相同结果
  • 做法:将这些测试合并为同一个表达式并将这个表达式提炼为独立函数
  • 原因:
    • 只是一次检查,只是存在并列条件需要检查而已
    • 为ExtractMethod(提炼函数)做准备,通过 函数名 告知“为什么这么做”
  • 特殊情形:条件表达式 存在副作用

9.3 ConsolidateDuplicateConditionalFragments(合并重复的条件片段)243

  • 情形:在条件表达式的分支上有相同的代码
  • 做法:将这段重复代码搬移到条件表达式之外,多行代码可以提炼为独立函数。
  • 当try和catch执行相同代码,可以将代码移到final区段。

9.4 RemoveControlFlag(移除控制标记)245

  • 情形:在一系列布尔表达式中,某个变量存在控制标记(control flag)作用。
  • 做法:以break或者continue代替

9.5 ReplaceNestedConditionalwithGuardClauses(以卫语句取代嵌套条件表达式)250

  • 情形:函数中的条件逻辑使人难以看清正确的执行路径。
  • 做法:使用卫语句表现特殊情况

9.6 ReplaceConditionalwithPolymorphism(以多态取代条件表达式)255

  • 情形:存在条件表达式根据对象的类型不同选择不同的行为
  • 做法:将表达式分支放进不同子类的重写方法,将原始函数提炼为抽象函数。

9.7 IntroduceNullObject(引入Null对象)260

  • 情形:需要再三检查对象是否为null
  • 做法:将null值替代为null对象

9.8 IntroduceAssertion(引入断言)267

  • 情形:某段代码需要对程序状态做出某种假设
  • 做法:以断言明确表现这种假设
  • 具体做法:
    • 断言在 发布的时候统统 被去除
    • 断言应该检查 ***一定必须为真 *** 的条件

第10章 简化函数调用271

10.1 RenameMethod(函数改名)273

  • 情形:命名不好
  • 做法:
    • 增加函数,将旧函数代码复制到新函数
    • 修改旧函数,让其转发调用新函数,如果旧函数引用点少可跳过
    • 编译测试
    • 找出旧函数引用,调用新函数
    • 编译测试
    • 删除旧函数

10.2 AddParameter(添加参数)275

  • 情形:某个函数需要调用端更多的信息
  • 做法:为此函数添加对象参数,让该对象带进函数所需信息。
  • 其他考虑:
    • 现有参数是否提供足够的信息?
    • 这个函数是否应该移动到拥有该信息的对象中?
    • 加入新参数是否合适,是否需要使用IntroduceParameterObject(引入参数对象)

10.3 RemoveParameter(移除参数)277

  • 情形:函数不需要某个参数
  • 做法:将该参数移除
  • 具体做法同10.1

10.4 SeparateQueryfromModifier(将查询函数和修改函数分离)279

  • 情形:某个函数既返回对象状态值,又修改对象状态
  • 做法:建立两个不同的函数,其中一个负责查询,另一个负责修改。
  • 原则:任何一个有返回值的函数都不应该有副作用。
  • 多线程:将修改和查询函数封装在一个同步函数中分开调用。

10.5 ParameterizeMethod(令函数携带参数)283

  • 情形:若干个函数做了类似的工作,但在函数本体中却包含了不同的值。
  • 做法:建立单一函数,以参数表达那些不同的值。
  • 要点: 可将少量数值视为参数

10.6 ReplaceParameterwithExplicitMethods(以明确函数取代参数)285

  • 情形:有个函数完全有数值不同采取不同的行为
  • 做法:针对该参数的每个可能值,建立独立函数。
  • 对比:与ParameterizeMethod(令函数携带参数)相反
  • 目的:提供清晰的入口。
  • 如果参数值对函数行为影响不大,不应该采用此方法。

10.7 PreserveWholeObject(保持对象完整)288

  • 情形:从某个对象取若干值,把他们作为参数传给函数
  • 做法:改为调用整个对象
  • 目的:避免过长参数
  • 不使用该方法:
    • 如果函数只依赖那些值不依赖对象,那么不能采用此方法,会导致耦合
    • 有时候函数使用了很多来自对象的数据,那么应该考虑使用(Move Method)

10.8 ReplaceParameterwithMethods(以函数取代参数)292

  • 情形:对象调用某个函数,并将所得结果作为参数传递给另一个函数,而接受该参数的函数本身也能够调用前一个函数
  • 做法:让参数接受者去除该项参数,并直接调用前一个函数

10.9 IntroduceParameterObject(引入参数对象)295

  • 情形:有些参数总是自然地同时出现
  • 做法:以一个对象取代这些参数
  • 目的:缩短参数长度,函数具有一致性,降低理解和修改代码的难度

10.10 RemoveSettingMethod(移除设值函数)300

  • 情形:类的某个字段应该对象创建的时候被设置,然后不再改变
  • 做法:去掉该字段的设置函数

10.11 HideMethod(隐藏函数)303

  • 情形:有一个函数,从来没有被任何类调用
  • 做法:将该函数设为private

10.12 ReplaceConstructorwithFactoryMethod(以工厂函数取代构造函数)304

  • 情形:创建对象时不仅仅是做简单的构建动作
  • 做法:将构造函数替换为工厂模式

10.13 EncapsulateDowncast(封装向下转型)308

  • 情形:某个函数返回的对象,需要由函数调用者执行向下转型()downcast
  • 做法:将向下转型移到函数中

10.14 ReplaceErrorCodewithException(以异常取代错误码)310

  • 情形:某个函数返回一个特定的代码,表示某个错误的情况
  • 做法:改用异常

10.15 ReplaceExceptionwithTest(以测试取代异常)315

  • 情形:面对一个调用者可以预先检查条件,你抛出了一个异常
  • 做法:修改调用者,使它在调用函数之前检查。

第11章 处理概括关系319

11.1 PullUpField(字段上移)320

  • 情形:两个子类拥有相同的字段
  • 做法:将该字段移动到超类,去除重复数据声明和关于数据的重复行为。并堆超类该字段使用-SelfEncapsulateField(自封装字段)

11.2 PullUpMethod(函数上移)322

  • 情形:有些函数,在各个子类产生相同的结果。
  • 做法:将该函数移动到超类

11.3 PullUpConstructorBody(构造函数本体上移)325

  • 情形:你在各个子类拥有一些构造函数,它们的本地几乎完全一致
  • 做法:在超类新建一个构造函数,并在子类构造函数中调用它。
  • 具体做法:
    • 将共同代码复制到超类构造函数中
    • 将共同代码放在子类构造函数起始处,然后再复制到超类构造函数中。
    • 将子类构造函数中共同代码删除,改用调用新建的超类构造函数。

11.4 PushDownMethod(函数下移)328

  • 情形:超类中的某个函数只与部分而非全部子类有关
  • 做法:将这个函数移到相关的子类去。

11.5 PushDownField(字段下移)329

  • 情形:超类中的某个字段只被部分而非全部子类使用
  • 做法:将这个字段移到需要它的那些子类去。

11.6 ExtractSubclass(提炼子类)330

  • 情形:类中的某些特性只被某些而非全部实例用到。
  • 做法:新建一个子类,将上面所说的那一部分特性移到子类中。
  • 具体情况:
    • 并不是出现类型码就表示需要用到子类,可以在委托和继承之间做选择。
    • 为子类新建构造函数,如果需要 隐藏子类,可使用ReplaceConstructorwithFactoryMethod(以工厂函数取代构造函数)
      • 找出超类调用点,如超类构造函数与子类不同,通过rename method方法可以解决。
      • 如果不需要超类实例,可以将超类声明为抽象类。
    • 逐一使用字段下移、函数下移将源类的特性移动到子类。

11.7 ExtractSuperclass(提炼超类)336

  • 情形:两个类有相似特性。
  • 做法:为两个类建立一个超类,将相同特性移至超类。

11.8 Extract Interface(提炼接口)341

  • 情形:某组用户只使用类责任区中一个特定子集或者两个类的接口有部分相同。
  • 做法:将相同子集提炼到独立的接口中。
  • 区别:提炼超类是提炼共同代码,提炼接口时提炼共同接口。
  • 具体情形:如果某个类在不同环境下扮演截然不同的角色,使用接口就是个好主意。

11.9 Collapse Hierarchy(折叠继承体系)344

  • 情形:超类和子类之间区别不大。
  • 做法:将它们合为一体。

11.10 Form TemPlate Method(塑造模板函数)344

  • 情形:你有一些子类,其中相应的函数以相同顺序执行类似的操作,但各个操作的细节有所不同。
  • 做法:将这些小操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也变得相同了。然后将原函数上移至超类,运用多态来避免重复代码。
  • 原因:虽然使用了继承,但是函数重复应尽量避免。

11.11 Replace inherited with Delegation(以委托取代继承)352

  • 情形:某个子类只使用超类接口中一部分,或是根本不需要继承而来的数据
  • 做法:在子类中新建一个字段用以保存超类,调整子类函数,令它委托超类,然后去掉两者之间的继承关系。

11.12 Replace Delegation with Inherited(以继承取代委托)352

  • 情形:在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数,
  • 做法:让委托类继承受托类。
  • 告诫:
    • 如果并没有使用受托类的所有函数,那么就不要使用这个方法。因为子类应该总是遵循超类的接口,如果委托过多可以通过移除“中间人”方法让客户端调用受托函数,或者“提炼超类”,让两个类的接口提炼到超类中。类似的还可以使用“提炼接口”方法。
    • 如果受托对象被不止一个其他对象共享,而且受托对象是可变的时候,那么这湿乎乎不能讲委托关系替换为继承关系。

……

第12章 大型重构359

第13章 重构,复用与现实379

第14章 重构工具401

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容