目录
坏代码的味道
何时必须重构?没有任何标准能比得上一个见识广博者的直觉。而某些迹象,则会指出“这里有可以用重构解决的问题”,一共22条坏代码味道。
Duplicated Code(重复代码)
如果你在一个以上的地点看到相同的程序结构,那么可以肯定,将它们合而为一,程序会变得更好。
最单纯的重复代码就是,同一个类的两个函数含有相同的表达式。这时需要采用Extract Method(提取方法)提取重复代码。
另一个常见的情况是,同个父类的两个子类内含有相同表达式。这是需要使用Extract Method(提取方法),然后使用Pull Up Method(方法上移),推入父类。
如果代码只是类似,并非完全形同,那么就得运用Extract Method(提取方法) 将相似部分和差异部分分割开,单独构成一个函数。
然后可以运用Form Template Method(表单模板方法)获得一个模板方法设计模式。
如果有些函数以不同的算法做相同的事,你可以选择其中较清晰的一个,并用Substitute Algorithm(替换算法)将其他函数替换掉
如果两个毫不相关的类出现Duplicated Code(重复代码),应该考虑对其中一个使用Extract Class(提炼类型),将复制代码提炼到一个独立类中,然后在另一个类使用这个新类。
但是重复代码所在的函数可能的确属于某个类,另一个类只能调用它。你必须决定这个函数放在那里最合适。
Long Method(过长函数)
程序越长越难理解。小函数容易理解的真正关键在于一个好名字,读者可以通过名字了解函数的作用。
最终效果是:应该更积极地分解函数。每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写在一个独立函数中,并以其用途命名。
大部分场合中,要把函数变小,只需要使用Extract Method(提炼函数)。找到函数中适合集中在一起的不分,把它们提炼出来形成一个新函数。
如果函数内有大量的参数和临时变量,他们会对你的函数提炼形成障碍。如果使用提取方法,会把变量当做参数传递。
此时,可以运用Replace Temp with Query(以查询取代临时变量)来消除这些临时元素。
Introduce Paramter Object(引入参数对象)和Preserve Whole Object(保持对象完整)可以将过长的参数列变得更简洁一些。
如果上诉方法无效,就应该使用 Replace Method with Method Object(以函数对象代替函数)
如何确定该提炼那一段代码?
一个很好的技巧就是寻找注释。通常能指出代码用途和实现手法之间的语义距离。
如果代码前方有一行注释,就是提醒你:可以将这段代码替换成一个函数,并且可以在注释的基础上给函数命名。
就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数去。
条件表达式和循环常常也是提炼的信号,可以使用Decompose Conditional(分解条件表达式)处理条件表达式。可以将循环和其内的代码提炼到一个独立函数中。
Large Class(过大的类)
如果利用单个类做太多事情,其内往往就会出现太多实例变量。Duplicated Code(重复代码)也就接踵而至。
可以运用Extract Class(提炼类型)将几个变量一起提炼到新类内。提炼时应选择类内彼此相关的变量。
通常如果类内的数个变量有着相同的前缀或字尾,就有机会把它们提炼到某个组件内。
如果这个组件适合作为一个子类,Extract Subclass(提炼子类)会比较简单。
有时候并非所有时刻都使用所有实例变量。那么可以多次使用Extract Class(提取类)或Extract Subclass(提取子类)。
Long Parameter List(过长参数列)
不必把函数需要的所有东西以参数传递,只需要给足够的,让函数能从中获取自己需要的东西就可以了。
如果向已有的对象发出一条请求就可以取代一个参数,那么应该使用Replace Parameter with Method(以函数代替参数)。
还可以运用Preserve Whole Object(保持对象完整)将来自同一个对象的一堆数据收集起来。
如果某些数据缺乏合理的对象归属,可以使用Interduce Parameter Object(引入参数对象)为它们制造一个参数对象。
如果你不希望造成“被调用对象”与“较大对象”间的某种依赖关系。使用参数也合情合理。
Divergent Change(发散式变化)
一旦需要修改,我们希望能够跳到系统的某一点,只在这个点做修改。如果不能做到这点,你就嗅出两种紧密相关的刺鼻中的一种了。
如果某个类经常因为不同的原因在不同的方向上发生变化,Divergent Change(发散式变化)就出现了。
同一个类中,如果新加一个数据库,需要修改三个函数。新加一个工具,修改四个函数。那么将这个对象分成两个会更好。每个对象可以只因一种变化而需要修改。
针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内所有内容都应该此变化。应该找出所有变化,然后使用Extract Class(提取类)
Shotgun Surgery(霰弹式修改)
如果遇到某种变化,你都必须在许多不同的类内做出许多小修改,那么你面临的坏味道就是Shotgun Surgery(霰弹式修改)。
如果需要修改的代码散布四处,你不但很难找到他们,也很容易忘记某个重要的修改。
这种情况下应该使用Move Method(搬移函数)和Move Field(搬移字段)把所有需要修改的代码放进同一个类。如果没有就创建一个。
(发散式变化)和(霰弹式修改)区别
发散式变化,是指“一个类受多种变化的影响”,霰弹式修改,是指“一种变化引发多个类相应修改”。
这两种情况你都会希望整理代码,使“外界变化”与“需要修改的类”一一对应。
Feature Envy(依恋情结)
函数对某个类的兴趣高过对自己所处类的兴趣。使用Move Method(搬移函数)把它移到该去的地方。称为依恋情结。
函数中只有一部分受依恋情结之苦,这时候应该使用Extract Method(提取方法)把这一部分提炼到独立函数中,在使用Move Method(搬移函数)
如果一个函数用到几个类的功能。原则是:判断哪个类拥有最多被此函数使用的数据,然后把这个函数和那些数据摆在一起。
策略模式和访问者模式,就是为了对抗Divergent Change(发散式变化)。原则:将总是一起变化的东西放在一块。
Data Clumps(数据泥团)
常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、函数签名中相同的参数。总是绑在一起出现的数据应该拥有它自己的对象。
找出这些数据以字段形式出现的地方,运用Extract Class(提取类)将他们提炼到一个独立对象中。然后将注意力转移到函数签名上。
运用Introduce Parameter Object(引入参数对象)或Preserve Whole Object(保持对象完整性)为他瘦身。
这么做的好处是可以将很多参数列缩短,简化函数调用。
Primitive Obsession(基本类型偏执)
可以运用Replace Data Value with Object(以对象取代数据值)将原本单独存在的数据值替换为对象。
如果想要替换的数据值是类型码,而他不影响行为,可以运用Replace Type Code with Class(以类取代类型码)替换掉。
如果你有与类型码相关的条件表达式,可以运用Replace Type Code with Subclasses(以子类取代类型码)或 Replace Type Code with State/Strategy(以状态模式/策略模式取代类型码)加以处理。
如果你有一组总是被放在一起的字段,可运用Extract Class(提炼类)。如果你在参数列中看到基本型数据,可运用Introduce Parameter Object(引入参数对象)。
如果你发现自己正从数组中挑选数据,可运用Replace Array with Object(以对象取代数组)
Switch Statements(switch惊悚现身)
少用switch语句,问题在于重复。经常会发现同样的switch语句散布于不同地点。如果要为它添加一个新的case,就必须找到所有的switch语句并修改它们。
使用面向对象中的多态概念可解决此方法。大多数时候,看到switch语句,就应该考虑以多态来替换它。
switch语句常常根据类型码进行选择。所以应该使用Extract Method(提炼函数)将switch语句提炼到一个独立函数中,再以Move Method(搬移函数)将它搬移到需要多态性的那个类里。
此时你必须决定是否使用Replace Type Code with Subclasses(以子类取代类型码)或Replace Type Code with State/Strategy(以状态模式/策略模式取代类型码)。
一旦完成继承结构后,就可以运用Replace Conditional with Plomorphism(以多态取代条件表达式)。
如果只是在单一函数中使用,并且不想改动它们。可以使用Replace Parameter with Explicit Methods(以明确函数取代参数)。
如果选择条件之一是null,可以使用Introduce Null Object(引入Null对象)
Parallel Inheritance Hierarchies(平行继承体系)
平行继承体系是霰弹式修改的特殊情况。在这种情况下每当你为某个类添加一个子类,也必须为另一个类添加相应的子类。
如果你发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同,便是问到了这种坏味道。
消除这种重复性的策略是,让一个继承体系引用另一个继承体系。或者使用Move Method(搬移函数)和Move Field(搬移字段)。
Lazy Class(冗赘类)
如果某些子类没有做足够的工作,可以使用Collapse Hierarchy(折叠继承体系)。
对于几乎没有用的组件,可以使用Inline Class(将类内联化)
Speculative Generality(夸夸其谈未来性)
如果某个抽象类其实没有太大作用,请使用Collapse Hierarchy(折叠继承体系)。
不必要的委托可运用Inline Class(将类内联化)除掉。
如果函数的某些参数未被使用上,可使用Remove Parameter(移除参数)
如果函数的名称带有多余的抽象意味,应该对他实施Rename Method(函数改名)
如果函数或类的唯一用户是测试用例,需要将它和对应的测试用例一并删掉。
Temporary Field(令人迷惑的暂时字段)
某个变量仅为某种特定情况而设。通常认为对象在所有时候都需要它的所有变量,在变量未被使用的情况下猜测当初其设置目的,会让你发疯。
如果类中有一个复杂算法,需要好几个变量。可以使用Extract Class(提炼类)还可以使用Introduce Null Object(引入Null对象)
Message Chains(过度耦合的消息链)
如果向一个对象请求另一个对象,这就是消息练。如果长串的消息链,意味着客户代码与查找过程中的导航结构紧密耦合。
一旦对象发生变化,客户端就不得不做出相应修改。这时应该使用Hide Delegate(隐藏委托关系)。
先观察消息链最终得到的对象是用来干什么的,看看能否Extract Method(提炼函数),再用Move Method(搬移函数)推入消息链。
Middle Man(中间人)
如果看到某个类接口有一半的函数都委托给其他类,这样就是过度运用。这个时候应该Remove Middle Man(移除中间人)
如果这样的函数很少,可以使用Inline Method(内联函数)把他们放进调用段。如果这些中间人还有其他行为,可以使用Replace Delegation with Inheritance(以继承取代委托)
Inappropriate Intimacy(狎昵关系)
看到两个类过于亲密,话费太多时间去探究彼此的private成分。过分亲密的类,可以使用Move Method(搬移函数)和Move Field(搬移字段)。
也可以看看是否可以运用Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)
如果很难分割两个类,可以使用Extract Class(提炼类)把两者共同点提炼到同一地点。或者使用Hide Delegate(隐藏委托关系)
如果子类对父类的继承关系不深,可以使用Replace Inheritance with Delegation(以委托取代继承)
Alternative Classes with Different Interfaces(异曲同工的类)
如果两个函数在做同一件事,却有着不同的签名,可以使用Rename Method(函数改名)根据用途重新命名。
请反复使用Move Method(搬移函数)将某些行为移入类,知道两者的协议一致位置。
如果必须重复移入代码才能完成这些,可以使用Extract Superclass(提炼超类)
Incomplete Library Class(不完美的库类)
如果只想修改库类的一两个函数,可以运用Introduce Foreign Method(引入外加函数)
如果想添加一大堆额外行为,就得运用Introduce Local Extension(引入本地扩展)
Data Class(数据类)
Data Class指拥有一些字段,以及用于访问的函数。除此之外一无长物。类似于model层。
如果这些类拥有public字段,应该运用Encapsulate Field(封装字段)
如果这些类内含有容器类的字段,应该检查是否进行了恰当的封装。如果没有就运用Encapsulate Collection(封装集合)
对于不该被其他类修改的字段,运用Remove Setting Method(移除设值函数)
然后找出这些函数被其他类调用的地点,尝试Move Method(搬移函数)把这些取值/设值隐藏起来
Refused Bequest(被拒绝的遗赠)
如果子类复用了父类的行为,却又不愿意支持父类的接口。这就意味着继承体系设计错误,需要新建一个子类,
然后使用Push Down Method(函数下移)和Push Down Field(字段下移)把所有用不到的函数下推给子类。
这样父类就只持有所有子类共享的东西。建议:所有父类都应该是抽象的
Comments(过多的注释)
如果你需要注释来解释一块代码做了什么,试试Extract Method(提炼函数)。
如果函数已经提炼出来了,但还是需要注释来解释其行为。试试Rename Method(函数改名)
如果你需要注释来说明某些系统的需求规格,试试Introduce Assertion(引入断言)
如果你不知道该做什么,这才是注释的良好运用动机
重构手法
Add Parameter(添加参数)
Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)
Change Reference to Value(将引用对象改为值对象)
Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)
Change Value to Reference(将值对象改为引用对象)
Collapse Hierarchy(折叠继承体系)
Consolidate Conditional Expression(合并条件表达式)
Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
Convert Procedural Design to Objects(将过程化设计转化为对象设计)
Decompose Conditional(分解条件表达式)
Duplicate Observed Data(复制被监视数据)
Encapsulate Collection(封装集合)
Encapsulate Downcast(封装向下转型)
Encapsulate Field(封装字段)
Extract Class(提炼类)
Extract Hierarchy(提炼继承体系)
Extract Interface(提炼接口)
Extract Method(提炼函数)
Extract Subclass(提炼子类)
Extract Superclass(提炼超类)
Form Template Method(塑造模板函数)
Hide Delegate(隐藏委托关系)
Hide Method(隐藏函数)
Inline Class(将类内联化)
Inline Method(内联函数)
Inline Temp(内联临时变量)
Introduce Assertion(引入断言)
Introduce Explaining Variable(引入解释性变量)
Introduce Foreign Method(引入外加函数)
Introduce Local Extension(引入本地扩展)
Introduce Null Object(引入Null对象)
Introduce Parameter Object(引入参数对象)
Move Field(搬移字段)
Move Method(搬移函数)
Parameterize Method(令函数携带参数)
Preserve Whole Object(保持对象完整性)
Pull Up Constructor Body(构造函数本体上移)
Pull Up Field(字段上移)
Pull Up Method(函数上移)
Push Down Field(字段下移)
Push Down Method(函数下移)
Remove Assignments to Parameter(移除对参数的赋值)
Remove Control Flag(移除控制标记)
Remove Middle Man(移除中间人)
Remove Parameter(移除参数)
Remove Setting Method(移除设值函数)
Rename Method(函数改名)
Replace Array with Object(以对象取代数组)
Replace Conditional with Plomorphism(以多态取代条件表达式)
Replace Constructor with Factory Method(以工厂函数取代构造函数)
Replace Data Value with Object(以对象取代数据值)
Replace Delegation with Inheritance(以继承取代委托)
Replace Error Code with Exception(以异常取代错误码)
Replace Exception with Test(以测试取代异常)
Replace Inheritance with Delegation(以委托取代继承)
Replace Magic Number with Symobolic Constant(以字面取代魔法数)
Replace Method with Method Object(以函数对象取代函数)
Replace Nested Conditional with Guard Clauses(以谓语句取代嵌套条件表达式)
Replace Parameter with Explicit Methods(以明确函数取代参数)
Replace Parameter with Method(以函数取代参数)
Replace Record with Data Class(以数据类取代记录)
Replace Subclass with Fields(以字段取代子类)
Replace Temp with Query(以查询取代临时变量)
Replace Type Code with Class(以类取代类型码)
Replace Type Code with State/Strategy(以状态模式/策略模式取代类型码)
Replace Type Code with Subclasses(以子类取代类型码)
Self Encapsulate Field(自封装字段)
Separate Domain from Presentation(将领域和表述/显示分离)
Separate Query from Modifier(将查询函数和修改函数分离)
Split Temporary Variable(分解临时变量)
Substitute Algorithm(替换算法)
Tease Apart Inheritance(梳理并分解继承体系)
坏代码味道及修改方法
Alternative Classes with Different Interfaces(异曲同工的类) : 函数改名,搬移函数
Comments(过多的注释):提炼函数,引入断言
Data Class(纯粹的数据类):搬移函数,封装字段,封装集合
Data Clumps(数据泥团):提炼类,引入参数对象,保持对象完整性
Divergent Change(发散式变化):提炼类
Duplicated Code(重复代码):提炼函数,提炼类,函数上移,塑造模板函数
Featrue Envy(依恋情结):搬移函数,搬移字段,提炼函数
Inappropriate Intimacy(狎昵关系):搬移函数,搬移字段,将双向关联改为单向关联,以委托代替继承,隐藏委托关系
Incomplete Library Class(不完美的类库):引入外加函数,引入本地扩展
Large Class(过大的类):提取类,提炼子类,提炼接口,以对象代替数组值
Lazy Class(冗赘类):将类内联化,折叠继承体系
Long Method(过长的函数):提炼函数,以查询取代临时变量,以对象取代函数,分解条件表达式
Long Parameter List(过长的参数列):以函数取代参数,引入参数对象,保持对象完整性
Message Chains(过度耦合的消息链):隐藏委托关系
Middle Man(中间人):移除中间人,内联函数,以继承取代委托
Parallel Inheritance Hierarchies(平行继承体系):搬移函数,搬移字段
Primitive Obsession(基本类型偏执):以对象取代数据值,提炼类,引入参数对象,以对象取代数组,以类取代类型码,以状态模式/策略模式取代类型码,以子类取代类型码
Refused Bequest(被拒绝的遗赠):以委托取代继承
Shotgun Surgery(霰弹式修改):搬移函数,搬移字段,类内联化
Speculative Generality(夸夸其谈未来性):折叠继承体系,将类内联化,移除参数,函数改名
Switch Statements(switch惊悚现身):以多态取代条件表达式,以子类取代类型码,以状态/策略模式取代类型码,以明确函数取代参数,引入Null对象
Temporary Field(迷惑的暂时字段):提炼类,引入Null对象
C#重构之道
定义
重构的定义:在不改变软件可观察行为的前提下,改善其内部结构。
其中,不改变软件行为,是重构最基本的要求。要想真正发挥威力,就必须做到“不需了解软件行为”。
如果一段代码能让你容易了解其行为,说明它还不是那么迫切需要被重构。
需要重构的代码,你只能看到其中的“坏味道”,接着选择手段消除这些“坏味道”,然后才有可能理解他的行为。
构建测试体系
确保每个类每个方法都有对应的测试代码。一套测试就是一套BUG检测器,能够大大缩减查找BUG所需时间。
编写测试代码最有效的时机,实在开始编程以前。能使你把注意力集中在接口而非实现上。
具体测试相关参见:单元测试与Moq
重构列表
重头戏来了,这个模块记录是实打实的重构方法。
介绍重构时,都采用一种标准格式,包含如下5个部分。
1.名称:建造一个重构词汇表,名称是很重要的。
2.概要:介绍此重构手法的使用情景,以及它所做的事情。
3.动机:介绍“为什么要使用这个重构”和“什么情况下不该使用这个重构”。
4.做法:简明扼要的一步一步介绍如何进行此重构。
5.范例:简单的例子说明此重构手法如何运作。
重新组织函数
很大一部分重构手法是对函数进行整理,使之更恰当地包装代码。这些问题几乎都源于“过长函数”。
对付过长函数,重要的手法就是“提炼函数”,把代码提取出来放到单独的函数中。“内联函数”,将调用动作替换为函数本体。
“提炼函数”的最大困难就是处理局部变量。处理函数时,运用“以查询取代临时变量”,尽可能去掉变量。
如果很多地方使用了临时变量,先运用“分解临时变量”将它们变得更容易替换。
有时候临时变量太混乱,需要使用“以函数取代参数”。可以分解最混乱的函数,代价则是引入一个新类。
参数带来的问题比临时变量少一些,如果你在函数里面赋值给他们,就要使用“移除对参数的赋值”
函数分解完毕后,也许会发现算法还可以改进,这时就使用“替换算法”引入更清晰的算法
提炼函数(Extract Method)
你有一段代码可以被组织在一起并独立出来。将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。
动机
当看到一个过长的函数或者一段需要注释才能让人理解用途的代码,我就会将这段代码放进一个独立函数中。
简短而命名良好的函数优点有:
1.如果每个函数的粒度都很小,那么函数被复用的机会就会更大。
2.这会使高层函数读起来就像是一系列注释。
3.如果函数都是细粒度,函数的覆写也会更容易。
长度不是函数大小的关键,关键在于函数名称和函数本体之间的语义距离。如果提炼可以强化代码的清晰度,那就去做。
做法
1.创建一个新函数,根据这个函数的意图来对他命名。(以它“做什么”来命名,而不是“怎么做”)
2.将提炼出的代码从源函数赋值到新建的目标函数中。
3.仔细检查提炼出的代码,看看是否引用了“作用域限于源函数”的变量。(包括局部变量和参数)
4.检查是否有“仅用于被提炼代码段”的临时变量,如果有在目标函数中将它们声明为临时变量。
5.检查代码段,看看是否有任何局部变量的值被他改变。如果被修改了,看看是否可以将被提炼的代码段处理为一个查询,并将结果赋值给相关变量。
如果很难这样做或者修改的变量不止一个,就需要先使用“分解临时变量”,然后再尝试提炼。也可以使用“以查询取代临时变量”
6.将被提炼的代码段中需要读取的局部变量,当做参数传递给目标函数。
7.处理完所有局部变量之后,进行编译。
8.将被提炼代码段替换为对目标函数的调用。
9.测试
范例一 无局部变量
最简单的情况
void printOwing()
{
double outstanding = 0.0;
//打印提示语
Console.WriteLine("**********");
Console.WriteLine("**Chenxy**");
Console.WriteLine("**********");
Console.WriteLine("standing:" + outstanding);
}
重构前
提炼函数,粘贴,在修改调用动作就可以了
void printOwing()
{
double outstanding = 0.0;
pringClues();
Console.WriteLine("standing:" + outstanding);
}
//打印提示语
void pringClues()
{
Console.WriteLine("**********");
Console.WriteLine("**Chenxy**");
Console.WriteLine("**********");
}
重构后
范例二 有局部变量
这个重构手法的难点在局部变量,包括源函数的参数,源函数所声明的临时变量。
局部变量简单的情况是,只是读取这些变量,并不修改他们。可以直接当做参数传递。
void printOwingBefore()
{
double outstanding = 0.0;
printClues();
Console.WriteLine("standing:" + outstanding);
} void printOwingAfter()
{
double outstanding = 0.0;
printClues();
printDetails(outstanding);
} private static void printDetails(double outstanding)
{
Console.WriteLine("standing:" + outstanding);
} //打印提示语
void printClues()
{
Console.WriteLine("**********");
Console.WriteLine("**Chenxy**");
Console.WriteLine("**********");
}
重构前/后
范例三 对局部变量再赋值
如果提炼的代码对局部变量赋值,就变得复杂了。如果你发现源函数的参数被赋值,应使用“移除对参数的赋值”
被赋值的临时变量,也分为两种。
较简单的情况是:只在被提炼代码段中使用。可以将声明移到被提炼代码段中,然后一起提炼出去。
另一种情况是:被提炼代码段之外的代码也使用了这个变量。
又分为两种:
1.再被提炼代码段之后,未被使用,直接在目标函数中修改即可。
2.在被提炼代码段之后,还是用了这个变量,就需要将目标函数返回该变量改变后的值。
void printOwingBefore()
{
double outstanding = 0.0;
printClues();
for (int i = ; i < ; i++)
{
outstanding++;
}
printDetails(outstanding);
} void printOwingAfter()
{
printClues();
double outstanding = getOutstanding();
printDetails(outstanding);
} double getOutstanding()
{
double result = 0.0;
for (int i = ; i < ; i++)
{
result++;
}
return result;
}
仅初始化
如果代码对这个变量还做了其他处理,就必须将它的值作为参数传递给目标函数。
void printOwingBefore(double previousAmount)
{
double outstanding = previousAmount * ;
printClues();
for (int i = ; i < ; i++)
{
outstanding++;
}
printDetails(outstanding);
} void printOwingAfter(double previousAmount)
{
printClues();
double outstanding = getOutstanding(previousAmount * );
printDetails(outstanding);
} double getOutstanding(double initialValue)
{
double result = initialValue;
for (int i = ; i < ; i++)
{
result++;
}
return result;
}
其他处理
如果返回的变量不止一个,有几种选择。
1.挑选另一块代码来提炼,让每个函数都只返回一个值,会安排多个函数,用以返回多个值。
2.使用out参数,带回多个返回值。
临时变量众多的情况下,尝试先运用“以查询取代临时变量”,如果这样做完以后还是很难提炼,就使用“以函数对象取代函数”
内联函数(Inline Method)
一个函数的本体与名称同样清楚易懂,在函数调用点插入函数本体,然后移除该函数。
动机
如果你的函数,内部代码和名字一样清晰易读,那么应该去掉这个函数,直接使用其中的代码。
另一种情况是你手上有一群组织不合理的函数,可以将他们内联到大型函数中,再从中提炼出组织合理的小型函数。
实施“以函数对象取代函数”之前先这么做,往往可以获得不错的效果。可以把所要的函数的所有调用对象的函数内容都内联到函数对象中。
如果别人使用了太多间接层,使得系统中所有函数都是对另一个函数的简单委托,这时就会使用“内联函数”。
做法
1.检查函数,确定它不具有多态性。
2.找出这个函数的所有被调用点
3.将这个函数的所有被调用点都替换为本体。
4.测试
5.删除这个函数
范例一
int _numberOfLateDeliveries = ;
int getRatingBefore() {
return moreThanFiveLateDeliveries() ? : ;
}
bool moreThanFiveLateDeliveries() {
return _numberOfLateDeliveries > ;
} int getRatingAfter() {
return (_numberOfLateDeliveries > ) ? : ;
}
lnline method
内联临时变量(Inline Temp)
有一个临时变量,只被一个简单表达式赋值一次,而他妨碍了其他重构手法。将所有对该变量引用动作,替换为对它赋值的那个表达式本身。
动机
内联临时变量,多半是作为“以查询取代临时变量”的一部分使用的,真正的动机出现在后者哪里。
唯一单独使用内联临时变量的情况是:你发现某个临时变量被赋予某个函数调用的返回值。一般来说不会有任何危害,
但如果这个临时变量妨碍了其他重构手法,例如“提炼函数”,就应该把它内联化。
做法
1.检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
2.将这个临时变量声明为readonly,检查是否真的只被赋值一次。
3.找到该临时变量所有引用点,将他们替换为“为临时变量赋值”的表达式。
4.测试
5.删除该临时变量的声明和赋值语句
范例一
bool getBasePriceBefore() {
double basePrice = anOrder.basePrice();
return (basePrice) > ;
}
bool getBasePriceAfter() {
return (anOrder.basePrice() > );
}
lnline Temp
以查询取代临时变量(Replace Temp With Query)
你的程序以一个临时变量保存某一个表达式的运算结果,将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。以后,新函数就可以被其他函数使用。
动机
临时变量的问题在于:他们是暂时的,而且只能在所属函数内使用。会驱使你写出更长的函数。
如果把临时变量替换为一个查询,那么同一个类中的所有函数都将可以获得这份信息。
“以查询取代临时变量”往往是你运用“提炼函数”之前必不可少的一个步骤。局部变量会使代码难以被提炼,所以你应尽可能的把它们替换为查询式。
这种重构手法较为简单的情况是:临时变量只被赋值一次,或者赋值给临时变量的表达式不受其他条件影响。
做法
1.找出只被赋值一次的临时变量,如果被赋值超过一次考虑使用“分解临时变量”将它分割成多个变量。
2.将变量声明为readonly
3.编译,查看是否只被赋值一次。
4.对临时变量赋值语句的等号右边部分提炼到一个独立函数中。首先将函数设为private
5.测试
6.在该临时变量实施“内联临时变量”。
范例一
int _quantity = , _itemPrice = ;
double getPriceBefore()
{
int basePrice = _quantity * _itemPrice;
double discountFactor;
if (basePrice > )
{
discountFactor = 0.95;
}
else
{
discountFactor = 0.96;
}
return basePrice * discountFactor;
}
double getPriceAfter()
{
return basePrice() * discountFacotr(); //使用内联临时变量
} private double discountFacotr()
{
if (basePrice() > )
{
return 0.95;
}
else
{
return 0.96;
}
} private int basePrice()
{
return _quantity * _itemPrice;
}
以查询取代临时变量
引入解释性变量(Introduce Explaining Variable)
你有一个复杂表达式,将该复杂表达式或其中一部分的结果放进一个临时变量,以此变量名称来解释表达式用途
动机
表达式有可能非常复杂而难以阅读。这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。
在条件逻辑中,引入解释性变量特别有价值:你可以用这项重构将每个子条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。
使用这项重构的另一个情况是:在较长的算法中,可以运用临时变量来解释每一步运算的意义。
做法
1.声明一个临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它。
2.将表达式中的“运算结果”这一部分,替换为上述临时变量。
3.测试
4.重复上述过程,处理表达式的其他部分。
范例一
int _quantity = , _itemPrice = ;
double priceBefore()
{
return _quantity * _itemPrice -
Math.Max(, _quantity - ) *
_itemPrice * 0.05 +
Math.Min(_quantity * _itemPrice * 0.1, 100.0);
} double priceAfter()
{
double basePrice = _quantity * _itemPrice;
double quantityDiscount = Math.Max(, _quantity - ) * _itemPrice * 0.05;
double shipping = Math.Min(basePrice * 0.1, 100.0);
return basePrice - quantityDiscount + shipping;
}
引入解释性变量
同样的解决方法,也可以使用“提炼函数”来处理
范例二
int _quantity = , _itemPrice = ;
double priceBefore()
{
return _quantity * _itemPrice -
Math.Max(, _quantity - ) *
_itemPrice * 0.05 +
Math.Min(_quantity * _itemPrice * 0.1, 100.0);
} double priceAfter()
{
return basePrice() - quantityDiscount() + shipping();
} private double shipping()
{
return Math.Min(_quantity * _itemPrice * 0.1, 100.0);
} private double quantityDiscount()
{
return Math.Max(, _quantity - ) *
_itemPrice * 0.05;
} private double basePrice()
{
return _quantity * _itemPrice;
}
提炼函数
选择“引入解释性变量”的契机是:如果要处理一个拥有大量局部变量的算法,那么使用“提炼函数”绝非易事。
这种情况下就使用“引入解释性变量”来理清代码,然后在考虑下一步怎么办。
搞清楚代码逻辑后,可以运用“以查询取代临时变量”把中间引入的那些解释性变量去掉。
分解临时变量(Split Temporary Variable)
你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。针对每次赋值,创造一个独立、对应的临时变量。
动机
临时变量中某些用途会很自然地导致临时变量被多次赋值。“循环变量”和“结果收集变量”就是两个典型例子。
除了这两种情况,还有很多临时变量用于保存一段冗长代码的运算结果。这种临时变量应该只被赋值一次。
如果他们赋值超过一次,就意味着它在函数中承担了一个以上的重任。如果承担多个责任,应该被分解为多个临时变量。每个变量只承担一个责任。
做法
1.在待分解临时变量的声明及其第一次被赋值处,修改其名称。如果赋值语句是 i=i+*,就意味着是结果收集变量。
2.以该临时变量的第二次赋值动作为界,修改此前对临时变量的所有引用点,让它们引用新的临时变量。
3.在第二次赋值处,重新声明原先那个临时变量。
4.测试
范例一
int _primaryForce = , _secondaryForce = , _mass = , _delay = ;
double getDistanceTravelledBefore(int time)
{
double result;
double acc = _primaryForce / _mass;
int primaryTime = Math.Min(time, _delay);
result = 0.5 * acc * primaryTime * primaryTime;
int secondaryTime = time - _delay;
if (secondaryTime > )
{
double primaryVel = acc * _delay;
acc = (_primaryForce + _secondaryForce) / _mass;
result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
}
return result;
}
double getDistanceTravelledAfter(int time)
{
double result;
double primaryAcc = _primaryForce / _mass;
int primaryTime = Math.Min(time, _delay);
result = 0.5 * primaryAcc * primaryTime * primaryTime;
int secondaryTime = time - _delay;
if (secondaryTime > )
{
double primaryVel = primaryAcc * _delay;
double secondaryAcc = (_primaryForce + _secondaryForce) / _mass;
result += primaryVel * secondaryTime + 0.5 * secondaryAcc * secondaryTime * secondaryTime;
}
return result;
}
分解临时变量
移除对参数的赋值(Remove Assignments to Parameters)
代码对一个参数进行赋值。以一个临时变量取代该参数的位置。
动机
对参数的赋值:如果把一个名为foo的对象作为参数赋值给某个函数,那么“对参数赋值” 意味着改变foo,使他引用另一个对象。
不推荐这样的做法,因为它降低了代码的清晰度,混用了按值传递和按引用传递这两种参数传递方式。
做法
1.建立一个临时变量,把待处理的参数赋值给它
2.以“对参数的赋值”为界,将其后所有对应此参数的引用点,全部替换为“对此临时变量的引用”
3.修改赋值语句,使其改为对新建之临时变量赋值
4.测试,如果代码的语以是按引用传递,请在调用后检查是否还使用了这个参数。
范例一
int discountBefore(int inputVal, int quantity, int yearToDate)
{
if (inputVal > ) inputVal -= ;
if (quantity > ) inputVal -= ;
if (yearToDate > ) inputVal -= ;
return inputVal;
}
int discountAfter(int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (inputVal > ) result -= ;
if (quantity > ) result -= ;
if (yearToDate > ) result -= ;
return result;
}
移除对参数的赋值
以函数对象取代函数(Replace Method with Method Object)
你有一个大型函数,其中对局部变量的使用使你无法采用“提炼函数”。将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。
然后你可以在同一个对象中将这个大型函数分解为多个小型函数。
动机
局部变量的存在会增加函数分解难度,如果一个函数中局部变量泛滥成灾,那么像分解这个函数时非常困难的。
“以查询取代临时变量” 可以助你减轻这一负担,但有时候你会发现根本无法拆解一个需要拆解的函数。这种时候就可以使用本重构方法。
“以函数对象取代函数” 会将所有局部变量都编程函数对象的字段。然后你就可以对这个新对象使用“提炼函数” 创造出新函数。
做法
1.建立一个新类,根据待处理函数的用途,为这个类命名。
2.在新类中新建一个源对象字段,用来保存原型大型函数所在的对象。针对原函数的每个临时变量和每个参数,建立一个对应的字段保存。
3.在新类中建立一个构造函数,接受源对象及原函数的所有参数作为参数。
4.在新类中建立一个compute() 函数。
5.将原函数的代码复制到compute() 函数中。如果需要调用对象的任何函数,请通过源对象字段调用
6.编译
7.将旧函数的函数本体替换为这样一条语句:创建上诉新类的一个新对象,而后调用其中的compute()函数
因为所有的局部变量现在都成了字段,所以你可以任意分解这个大型函数,不必传递任何参数。
范例一
public class Account
{
int detail()
{
return ;
}
public int gamaaBefore(int inputVal, int quantity, int yearToDate)
{
int importantValue1 = (inputVal * quantity) + detail();
int importantValue2 = (inputVal * yearToDate) + ;
if ((yearToDate - importantValue1) > )
{
importantValue2 -= ;
}
int importantValue3 = importantValue2 * ;
return importantValue3 - * importantValue1;
}
}
重构前
为了把这个函数变成函数对象,需要声明一个新类。新字段保存源对象。
class Gamma
{
private Account _account;
private int inputValue;
private int quantity;
private int yearToDate;
private int importantValue1;
private int importantValue2;
private int importantValue3;
public Gamma(Account source, int inputValueArg, int quantityArg, int yearToDateArg)
{
_account = source;
inputValue = inputValueArg;
quantity = quantityArg;
yearToDate = yearToDateArg;
}
public int compute()
{
importantValue1 = (inputValue * quantity) + _account.detail();
importantValue2 = (inputValue * yearToDate) + ;
importantThing();
importantValue3 = importantValue2 * ;
return importantValue3 - * importantValue1;
} private void importantThing()
{
if ((yearToDate - importantValue1) > )
{
importantValue2 -= ;
}
}
}
public class Account
{
public int detail()
{
return ;
}
public int gamaaBefore(int inputVal, int quantity, int yearToDate)
{
return new Gamma(this, inputVal, quantity, yearToDate).compute();
}
}
重构后
替换算法(Substitute Algorithm)
你想要把某个算法替换为另一个更清晰的算法。将函数本体替换为另一个算法。
动机
如果你发现做一件事可以有更清晰的方式,就应该以较清晰的方式取代复杂的方式。
有时候你会想修改原先的算法,让他去做一件与原先略有差异的事。
做法
1.准备好另一个算法,让他通过编译。
2.针对现有测试,执行上述的新算法。如果结果与原本结果相同,重构结束。
3.如果测试结果不同于原先,在测试和调用过程中,依旧算法为比较参照标准。
范例一
string foundPersonBefore(string[] people)
{
for (int i = ; i < people.Length; i++)
{
if (people[i].Equals("Don"))
{
return "Don";
}
}
return "";
}
string foundPersonAfter(string[] people)
{
List<string> candidates = new List<string> { "Don" };
for (int i = ; i < people.Length; i++)
{
if (candidates.Contains(people[i])) {
return people[i];
}
}
return "";
}
替换算法
在对象之间搬移特性
在对象设计过程中,“决定把责任放在那里” 是最重要的事之一。
常常我们需要只是用“搬移函数” 和 “搬移字段”。如果两个重构手法都需要用到,会首先使用“搬移字段” 在使用“搬移函数”
类往往会因为承担过多责任而变得臃肿不堪,使用“提炼类” 将一部分责任分离出去。
如果一个类变得太“不负责任”,我就会使用“将类内联化” 将它融于另一个类。
如果一个类使用另一个类,运用“隐藏委托关系” 将这种关系隐藏起来通常是有帮助的。
有时候隐藏委托会导致拥有者的接口经常变化,需要使用“移除中间人”
当我不能访问某个类的源码,却又想把其他责任移进这个不可修改类时,我才会使用“引入外加函数” 和 “引入本地扩展”
如果我想加入的只是一或两个函数,就会使用“引入外加函数”,不止一两个函数,就是用“引入本地扩展”
搬移函数(Move Method)
你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。
在该函数最长引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。
动机
“搬移函数”是重构理论的支柱,如果一个类有太多行为或一个类与另一个类有太多合作而形成高度耦合,就应该搬移函数。
使用另一个对象的次数比使用自己所驻对象的次数还多。一旦移动了一些字段,就该做这样的检查。
一旦发现有可能搬移的函数,就会观察调用它的那一端,它调用的那一端,以及集成体系中它任何一个自定义函数。
然后根据“这个函数与那个对象的交流比较多”,决定其移动路径。
做法
1.检查源类中被源函数所使用的一切特性,考虑它们是否也该被搬移。
2.检查源类的子类和父类,看看是否有该函数的其他声明。
3.在目标类中声明这个函数。
4.将源函数的代码复制到目标函数中,调整后者,使其能在新家中正常运行。
5.编译目标类
6.决定如何从源函数正确引用目标对象
7.修改源函数,使之成为一个纯委托函数。
8.测试。
9.决定是否删除源函数
10.如果要移除源函数,请将源类中对源函数的所有调用,替换为目标函数的调用。
范例一
public class Account
{
private AccountType _type;
private int _daysOverdrawn;
double overdraftCharge()
{
if (_type.isPremium())
{
double result = ;
if (_daysOverdrawn > )
{
result += (_daysOverdrawn - ) * 0.85;
}
return result;
}
else
{
return _daysOverdrawn * 1.75;
}
}
double bankCharge() {
double result = 4.5;
if (_daysOverdrawn > )
result += overdraftCharge();
return result;
}
}
重构前
首先观察overdraftCharge使用的每一项特性,考虑是否值得将它们一起移动。
当我们需要使用源类特性时,有4种选择
1.将这个特性也移到目标类
2.建立或使用一个从目标类到源类的引用关系。
3.将源对象当做参数传递给目标函数。
4.如果所需特性是个变量,将它当做参数传递目标函数。
public class AccountType
{
public bool isPremium()
{
return true;
}
public double overdraftCharge(int daysOverdrawn)
{
if (isPremium())
{
double result = ;
if (daysOverdrawn > )
{
result += (daysOverdrawn - ) * 0.85;
}
return result;
}
else
{
return daysOverdrawn * 1.75;
}
}
}
public class Account
{
private AccountType _type;
private int _daysOverdrawn;
double bankCharge() {
double result = 4.5;
if (_daysOverdrawn > )
result += _type.overdraftCharge(_daysOverdrawn);
return result;
}
}
重构后
如果需要的源类有多个特性,那么我也会将源对象传递给目标函数。不过如果目标函数需要太多源类特性,就值得进一步重构。
这种情况下,我会分解目标函数,并将其中一部分移回源类。
搬移字段(Move Field)
某个字段被其所驻类之外的另一个类更多地用到。在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段。
动机
在类之间移动状态和行为,是重构过程中必不可少的措施。
如果我发现,对于一个字段,在其所驻类之外的另一个类中有更多函数使用了它,我就会考虑搬移这个字段。
使用“提炼类”时,可能需要搬移字段。此时会先搬移字段,然后再搬移函数。
做法
1.如果字段的访问级别是public,使用“封装字段”。如果你有可能移动那些频繁访问字段的函数,先使用“自封装字段”
2.测试
3.在目标类中建立与源字段相同的字段,并同时建立相应的设值/取值函数。
4.编译目标类
5.决定如何在源对象中引用目标对象。首先看是否有现成的字段或函数。在看能否轻易建立这样一个函数。不行,就得在源类中新建一个字段来存放目标。
6.删除源字段
7.将所有对源字段的引用替换为对某个目标函数的调用。
8.测试
范例一
public class AccountType
{
}
public class Account
{
private AccountType _type;
private double _interestRate;
double interestForAmount_days(double amount, int days)
{
return _interestRate * amount * days / 365l
}
}
重构前
public class AccountType
{
private double _interestRate; public double InterestRate
{
get
{
return _interestRate;
} set
{
_interestRate = value;
}
}
}
public class Account
{
private AccountType _type;
double interestForAmount_days(double amount, int days)
{
return _type.InterestRate * amount * days / ;
}
}
重构后
范例二 使用自封装字段
如果很多函数已经使用了_interestRate字段,应该先运用“自我封装”
public class AccountType
{
private double _interestRate; public double InterestRate
{
get
{
return _interestRate;
} set
{
_interestRate = value;
}
}
}
public class Account
{
private AccountType _type;
double interestForAmount_days(double amount, int days)
{
return GetInterestRate() * amount * days / ;
}
private void SetInterestRate(double arg) {
_type.InterestRate = arg;
}
private double GetInterestRate() {
return _type.InterestRate;
}
}
重构后
以后若有必要,我可以修改访问函数的用户,让它们使用新对象。自封装字段,使我得以保持小步前进。
首先使用“自封装字段”使得更轻松的使用“搬移函数”将函数搬移到目标类中。
提炼类(Extract Class)
某个类做了应该由两个类做的事情。建立一个新类,将相关的字段和函数从旧类搬移到新类。
动机
一个类应该是一个清楚的抽象,处理一些明确的责任。给某个类添加一项新责任时,你会觉得不值得为这项责任分离一个单独的类。
于是,随着责任的不断添加。这个类会变得非常复杂。往往含有大量函数和数据,不易理解。此时需要考虑哪些部分可以分离出去。
如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表示你应该将它们分离出去。
如果你发现子类化只影响类的部分特性,或如果发现某些特性需要以一种方式来子类化,某些需要另一种子类化。这就意味你要分解原来的类。
做法
1.决定如何分解类所负的责任。
2.建立一个新类,用以表现从旧类中分离出来的责任。
3.建立“从旧类访问新类”的链接关系。有可能双向连接,但在需要他之前,不要建立“从新类通往旧类”的链接
4.对于你想搬移的每一个字段,运用“搬移字段”
5.每次搬移后,测试
6.使用“搬移方法”将必要的函数搬移到新类。先搬移较底层函数,在搬移较高层函数
7.每次搬移后,测试
9.检查每个类的接口,如果建立起双向连接,检查是否可以将它改为单向链接。
10.决定是否公开新类,如果你需要公开他,就要决定让他成为引用对象还是不可变的值对象。
范例一
public class Person
{
private string _name;
private string _officeAreaCode;
private string _officeNumber;
public string getName()
{
return _name;
}
public string getTelephoneNumber()
{
return ("(" + _officeAreaCode + ")" + _officeNumber);
}
string getOfficeAreaCode()
{
return _officeAreaCode;
}
void setOfficeAreaCode(string arg)
{
_officeAreaCode = arg;
}
string getOfficeNumber()
{
return _officeNumber;
}
void setOfficeNumber(string arg)
{
_officeNumber = arg;
}
}
重构前
首先将于电话号码相关的行为分离到一个独立类中。新建TelephoneNumber类
建立Person到TelephoneNumber之间的链接
使用“搬移字段”移动字段
使用“搬移函数”将相关函数移动到新类中
class TelephoneNumber
{
private string _areaCode;
private string _number;
public string getAreaCode()
{
return _areaCode;
}
public void setAreaCode(string arg)
{
_areaCode = arg;
}
public string getNumber()
{
return _number;
}
public void setNumber(string arg)
{
_number = arg;
}
public string getTelephoneNumber()
{
return ("(" + getAreaCode() + ")" + getNumber());
}
} public class Person
{
private string _name;
private TelephoneNumber _officeTelephone = new TelephoneNumber();
public string getName()
{
return _name;
}
public string getTelephoneNumber()
{
return _officeTelephone.getTelephoneNumber();
}
string getOfficeAreaCode()
{
return _officeTelephone.getAreaCode();
}
void setOfficeAreaCode(string arg)
{
_officeTelephone.setAreaCode(arg);
}
string getOfficeNumber()
{
return _officeTelephone.getNumber();
}
void setOfficeNumber(string arg)
{
_officeTelephone.setNumber(arg);
}
}
重构后
下一步决定要不要对用户公开这个新类,可以将Person中与电话号码相关的函数委托到TelephoneNumber。
也可以直接对用户公开,将它公开部分给用户。如果选择公开,需要考虑别名的风险。
面对这种问题,有下列几种选择
1.允许任何对象修改TelephoneNumber对象的任何部分。就当做引用对象,考虑使用“将值对象改为引用对象”
2.不允许任何人不通过Person对象就修改TelephoneNumber对象。
将类内联化(lnline Class)
某个类没有做太多事情。将这个类的所有特性搬移到另一个类中,然后移除原类。
动机
如果一个类不在承担足够责任、不再有单独存在的理由,就会以“类内联化”手法将类塞进另一个类中。
做法
1.如果在目标类身上声明源类的public协议,将其中所有函数委托至源类。
2.修改所有源类引用点,改而引用目标类
3.测试
4.运用“搬移字段”和“搬移方法”,将源类的特性全部搬移到目标类。
范例一
class TelephoneNumber
{
private string _areaCode;
private string _number;
public string getAreaCode()
{
return _areaCode;
}
public void setAreaCode(string arg)
{
_areaCode = arg;
}
public string getNumber()
{
return _number;
}
public void setNumber(string arg)
{
_number = arg;
}
public string getTelephoneNumber()
{
return ("(" + getAreaCode() + ")" + getNumber());
}
} public class Person
{
private string _name;
private TelephoneNumber _officeTelephone = new TelephoneNumber();
public string getName()
{
return _name;
}
public string getTelephoneNumber()
{
return _officeTelephone.getTelephoneNumber();
}
TelephoneNumber getOfficeTelephone() {
return _officeTelephone;
}
}
重构前
首先声明TelephoneNumber的所有可见函数。
运用搬移字段和搬移方法,将源类的特性搬移到目标类。
重构后代码略
隐藏委托关系(Hide Delegate)
客户通过一个委托类来调动另一个对象,在服务类上建立客户所需的所有函数,用以隐藏委托关系。
动机
封装是对象最关键特征之一,意味着每个对象都应该尽可能少了解系统的其他部分。一旦发生变化,需要了解这一变化的对象就会比较少。
如果某个客户先通过服务对象的字段得到另一个对象,然后调用后者的函数,那么客户就必须知晓这一层委托关系。
万一委托发生改变,客户也得相应变化,你可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖。
这样,即便将来发生委托关系上的变化,变化也将被限制在服务对象中,不会波及客户。
做法
1.对于每一个委托关系中的函数,在服务对象端建立一个简单的委托函数。
2.调整客户,令他只调用服务对象提供的函数。
3.每次调整后,编译测试
4.如果将来不再有任何客户需要取用委托类,便可移除服务对象中的相关访问函数
5.测试
范例一
public class Department
{
Person _manager;
public Person Manager
{
get
{
return _manager;
} set
{
_manager = value;
}
}
}
public class Person
{
Department _department;
public Department Department
{
get
{
return _department;
} set
{
_department = value;
}
}
}
重构前
如果希望知道某人部门的Manage,必须先取得Department对象。这就暴露了Department工作原理。
为了这个目的,我们在Person中建立一个简单的委托函数。
public class Person
{
Department _department;
public Department Department
{
get
{
return _department;
} set
{
_department = value;
}
}
public Person getManager() {
return Department.Manager;
}
}
重构后
然后修改所有引用即可。
移除中间人(Remove Middle Man)
某个类做了过多的简单委托动作,让客户直接调用收委托类。
动机
在“隐藏委托”中,说到了封装受托对象的好处,但是这层封装的代码就是,每当用户要使用受托类的新特征时,你就必须添加一个简单委托。
随着受托类功能越来越多,这一过程就会让你痛苦不已。服务类完全变成中间人,此时就应该让客户直接调用受托类。
什么程度的隐藏才是合适的,这个很难说。你可以在系统运行过程中不断进行调整,随着系统变化,尺度也在进行变化。
做法
1.建立一个函数,用以获得委托对象。
2.对于每个委托函数,在服务类中删除该函数,并让需要调用该函数的客户转为调用受托对象。
3.处理每个委托函数后,测试
范例一
public class Department
{
public Person Manager { get; set; }
}
public class Person
{
public Department Department { get; set; }
public Person getManager()
{
return Department.Manager;
}
}
重构前
调用重构前的方法 manager = john.getManager(); 使用和封装Department都很简单,但如果大量函数都这么做,我就不得不在Person之中安置大量委托行为。
这就该是移除中间人的时候了。
public class Department
{
public Person Manager { get; set; }
}
public class Person
{
public Department Department { get; set; }
}
重构后
这时调用方式为 manager = john.Department.Manager
引入外加函数(Introduce )
你需要为提供服务的类增加一个函数,但你无法修改这个类。在客户类中建立一个函数,并以第一参数形式传入一个服务类实例
动机
你正在使用一个类,为你提供了需要的所有服务,当你又需要一个新服务的时候,这个类却无法供应。
如果客户类只使用这项功能一次,额外编码工作没什么大不了。然而如果你需要使用多次这个函数,就得不断重复这些代码。
进行本重构时,如果你以外加函数实现一项功能。那么“这个函数原本应该在提供服务的类中实现”
做法
1.给客户类中建立一个函数,用来提供你需要的功能。这个函数不应该调用客户类的任何特性,如果需要值,用参数传递
2.以服务类实例作为该函数的第一个参数。
3.将该函数注释为“外加函数,应在服务类实现”
范例
public void PreviousBefore()
{
DateTime newStart = new DateTime(previousEnd.Year, previousEnd.Month, previousEnd.Day + );
}
public void PreviousAfter() {
DateTime newStart = nextDay(previousEnd);
} private DateTime nextDay(DateTime previousEnd)
{
return new DateTime(previousEnd.Year, previousEnd.Month, previousEnd.Day + );
}
收费周期
将赋值运算符右侧代码提炼到一个独立函数中,这个函数就是DateTime类的外加函数。
引入本地扩展(Introduce Local Extension)
你需要为服务类提供一些额外函数,但你无法修改这个类。建立一个新类,使他包含这些额外函数。让这个扩展品成为源类的子类或包装类。
动机
如果不能修改源码,并且需要的额外函数超过两个。就可以使用引入本地扩展
做法
引入this关键字,作为扩展方法使用。
重新组织数据
自封装字段(Self Encapsulate Field)
你直接访问一个字段,但与子弹之间的耦合关系主键变得笨拙。为这个字段建立取值/设值函数,并且只以这些函数来访问字段。
这个,没有什么可写的。使用属性即可。
以对象取代数据值(Replace Data Value with Object)
你有一个数据项,需要与其他数据和行为一起使用才有意义。将数据项变成对象。
动机
往往决定以简单的数据项表示简单的情况。但是随着开发的进行,你可能会发现,这些简单的数据不再简单了。
比如,一开始你用一个字符串来表示“电话号码”,但是随后你会发现,电话号码需要“格式化”“抽取区号”之类的特殊行为。
如果这样的数据只有一两个,你还可以把相关函数放进数据项所属的对象里。但是,坏味道的“重复代码” 和“依恋情结”很快就会出现。
这个时候就应该将数据值变成对象。
做法
1.为带替换数据新建一个类,声明一个字段,其类型和源类中的待替换数值类型一样。然后在新类中加入这个字段的取值函数,在加上接受此字段的构造函数。
2.编译
3.将源类中的待替换数值字段的类型改为前面新建的类。
4.修改源类中该字段的取值函数,令他调用新类的取值函数。
5.如果源类构造函数中用到了这个待替换字段,修改构造函数,令他改用新类的构造函数来对字段赋值。
6.修改源类中待替换字段的设置函数,令他为新类创建一个实例
7.测试
8.可能需要对新类使用“将值对象改为引用对象”
范例
有一个代表“订单”的Order类,以一个字符串记录订单客户。希望改用一个对象来表示客户信息,这样就有充裕的弹性保存客户地址,信用等信息。
class Order {
public string Customer { get; set; }
public Order(string customerArg)
{
Customer = customerArg;
}
}
重构前
首先,新建一个Customer类来表示客户概念。然后建立字段,保存一个字符串。
class Order
{
public Order(string customerArg)
{
_customer = new Customer(customerArg);
}
private Customer _customer;
public string Customer
{
get
{
return _customer.getName();
}
set
{
_customer = new Customer(value);
}
}
}
class Customer
{
private string _name;
public Customer(string name)
{
_name = name;
}
public string getName()
{
return _name;
}
}
重构后
将值对象改为引用对象
你从一个类衍生出许多彼此相等的实例,希望将它们希望将他们替换为同一对象。将这个值对象变成引用对象。
动机
有时候,你会从一个简单的值对象开始,在其中保存少量不可修改的数据。而后你可能会希望给这个对象加一些可修改数据,
并确保对任何一个对象的修改都能影响到所有引用此一对象的地方。这时候你就需要将这个对象变成一个引用对象。
做法
1.使用“以工厂函数取代构造函数”
2.编译,测试
3.决定由什么对象负责提供访问新对象的途径。可能是静态字典或注册表对象。也可以使用多个对象作为新对象的访问点。
4.决定这些引用对象应该预先创建好,或是应该动态创建。
5.修改工厂函数,令他返回引用对象。
6.测试
范例
internal class Order
{
public Order(string customerArg)
{
_customer = new Customer(customerArg);
} private Customer _customer; public string Customer
{
get
{
return _customer.getName();
}
set
{
_customer = new Customer(value);
}
}
} internal class Customer
{
private string _name; public Customer(string name)
{
_name = name;
} public string getName()
{
return _name;
}
}
重构前
Customer对象是值对象。就算多份订单属于同一客户,每个Order对象还是拥有各自的Customer对象。
可以做一下改变,使得一旦同一客户拥有多份不同订单,代表这些订单的所有Order对象就可以共享同一个对象。
每一个客户只该对应一个Customer对象。
首先使用“以工厂函数取代构造函数”,这样就可以控制Customer对象的创建过程。先定义这个工厂函数。
然后修改构造函数声明private
修改访问对象方式
internal class Order
{
public Order(string customerArg)
{
_customer = Customer.create(customerArg);
} private Customer _customer; public string Customers
{
get
{
return _customer.getName();
}
set
{
_customer = Customer.create(value);
}
}
} internal class Customer
{
private string _name;
private static Dictionary<string, Customer> _instances = new Dictionary<string, Customer>();
public string name { get; set; } private Customer(string name)
{
_name = name;
} public string getName()
{
return _name;
} public static Customer create(string name)
{
if (!_instances.Any(p => p.Key.Equals(name)))
{
new Customer(name).store();
}
return _instances[name];
} private void store()
{
_instances.Add(this.getName(), this);
}
}
重构后
将引用对象改为值对象(Change Reference to Value)
你有一个引用对象,很小且不可变,而且不容易管理。将它变成一个值对象。
动机
如果引用对象开始变得难以使用,就应该将它改为值对象。引用对象必须被某种方式控制,你总是必须向其控制者请求适当的引用对象。
他们可能造成内存区域之间错综复杂的关联。在分布系统和并发系统中,不可变的值对象特意有用,因为你无需考虑他们的同步问题。
值对象有一个非常重要的特征:他们应该是不可变的,无论何时只要你调用同一对象的同一查询函数,都应该得到同样的结果。
略,这节没明白什么意思
以对象取代数组(Replace Array with Object)
你有一个数组,其中的元素各自代表不同的东西。以对象替换数组,对于数组中的每个元素,以一个字段来表示。
动机
如果一个数组容纳了多种不同的对象,就需要使用对象来取代数组。
做法
1.新建一个类表示数组所拥有的信息,并在其中一个public字段保存原先的数组。
2.修改数组的所有用户,让他们改用新类的实例。
3.编译,测试
4.逐一为数组元素添加取值/设置函数。根据元素的用途,为这些访问函数命名。修改客户端代码,让他们通过访问函数取用数组内的元素。
5.当所有对数组的直接访问都转而调用访问函数后,将新类中保存该数组的字段声明为private。
6.编译
7.对于数组内的每一个元素,在新类中创建一个类型相当的字段。修改该元素的访问函数,令他改用上诉的新建字段。
8.每修改一个元素,编译并测试
9.数组的所有元素都有了相应字段之后,删除该数组。
范例
private static void Main(string[] args)
{
//重构前使用数组
string[] row = new string[];
row[] = "Liverpool";
row[] = ""; //使用对象
Performance row1 = new Performance();
row1.Name = "Liverpool";
row1.Wins = "";
Console.WriteLine(row1.Name);
Console.WriteLine(row1.GetWins());
Console.ReadKey();
} private class Performance
{
public string Name { get; set; }
private string _wins;
public string Wins { set { _wins = value; } } public int GetWins()
{
return int.Parse(_wins);
}
}
重构后
以字面常量取代魔法数(Replace Magic Number with Symbolic Constant)
有一个字面数值,带有特别含义。创建一个常量,根据其意义为他命名,并将沙河南高速的字面数值替换为这个常量。
动机
魔法数是历史最悠久的不良现象之一,指拥有特殊意义,却不能明确表现出这种意义的数字。
如果你在不同的地点引用同一个逻辑树,魔法数会让你烦恼不已,因为一旦这些数改变,就必须在程序中找到所有魔法数。
做法
使用const将数变成常量
以类取代类型码(Replace Type Code with Class)
类之中有一个数值类型码,但它并不影响类的行为。以一个新的类替换该数值类型码。
动机
类型码或枚举值,如果带着一个有意义的符号名还是不错的。但无法进行类型验证。任何接受类型码作为参数的函数,所期望的实际上还是一个数值,无法强制使用符号名。这回降低代码可读性,从而产生BUG。
类型码指的是,特殊名称的变量,例如:int O = 0,A=1;
如果把数值换成一个类,编译器就可以对这个类进行类型验证。只要提供工厂函数,就可以保证只有合法的实例才会被创建出来。
只有当类型码是纯粹数据时(不会再switch语句中引起行为变化),才能以类来取代它。switch或者if,只能使用常量进行判断,无法根据类进行判断。
做法
1.为类型码建立一个类,需要有一个用以记录类型码的字段。并应该有对应的取值函数,用静态变量保存允许被创建的实例。
2.修改源类实现,让它使用上述新建类。
3.编译,测试
4.对于源类中的每一个使用类型码的函数,相应建立一个函数,让新函数使用新建的类。
5.逐一修改源类用户,让它们使用新接口。
6.每修改一个用户,编译并测试
7.删除使用类型码的久接口,并删除旧类型码的静态变量。
8.编译,测试
范例
每个人都拥有四种血型的一种,使用Person表示人,其中类型码表示血型。
class Person
{
public static int O = ;
public static int A = ;
private int _bloodGroup;
public Person(int bloodGroup)
{
_bloodGroup = bloodGroup;
}
public void setBloodGroup(int arg)
{
_bloodGroup = arg;
}
public int getBloodGroup()
{
return _bloodGroup;
}
}
重构前
首先,建立一个新的类BloodGroup,表示血型,并保存原本的类型码。
然后,修改Person中的类型码改为使用BloodGroup类
修改Person用户,让它们以BloodGroup对象表示类型码,而不再使用整数。
public class Program
{
private static void Main(string[] args)
{
Person thePerson = new Person(BloodGroup.A);
thePerson.getBloodGroup().getCode();
thePerson.setBloodGroup(BloodGroup.O);
Console.ReadKey();
}
} class Person
{
private BloodGroup _bloodGroup;
public Person(BloodGroup bloodGroup)
{
_bloodGroup = bloodGroup;
}
public void setBloodGroup(BloodGroup arg)
{
_bloodGroup = arg;
}
public BloodGroup getBloodGroup()
{
return _bloodGroup;
}
} class BloodGroup
{
public static BloodGroup O = new BloodGroup();
public static BloodGroup A = new BloodGroup();
private int _code;
private BloodGroup(int code)
{
_code = code;
}
public int getCode()
{
return _code;
}
}
重构后
以子类取代类型码(Replace Type Code with Subclasses)
你有一个不可变的类型码,它会影响类的行为。以子类取代这个类型码。
动机
如果类型码不会影响宿主类的行为,可以使用“以类取代类型码”,但如果类型码会影响宿主类的行为,那么最好使用此重构,借助多态来处理变化行为。
这种情况的标志就是像switch这样的条件表达式。这种表达式有两种表现形式。
1.switch 2.if then elase 结构。
无论那种形式,他们都是检查类型码值,并根据不同的值执行不同的动作。这种情况下,应该使用“以多态取代表达式”进行重构。
但为了能够顺利进行那样的重构,首先应该将类型码替换为可拥有多态行为的继承体系。应该以类型码的宿主为基类,并针对每一种类型码各建立一个子类。
但是以下两种情况你不能这么做:1.类型码值在对象创建之后发生了改变。2.由于某些原因,类型码宿主类已经有了子类。
如果你恰好面临这两种情况之一,就需要使用“以状态模式/策略模式取代类型码”
使用此重构的另一个原因是,宿主类中出现“只与具备特定类型码之对象相关”特性。完成本重构后,可以使用“字段下移”和“函数下移”推到子类中。
做法
1.使用“自封装字段”将类型码自我封装起来。如果类型码被传递给构造函数,就需要将构造函数换成工厂函数。
2.为类型码的每一个数值建立相应子类。在每个子类中覆写类型码的取值函数,使其返回相应的类型码。
3.每建立一个子类,编译并测试
4.从父类中删除保存类型码的字段,将类型码访问函数声明为抽象函数。
5.编译,测试
范例
Employee表示雇员
class Employee
{
private int _type;
static int ENGINEER = ;
static int SALESMAN = ;
static int MANAGER = ;
Employee(int type)
{
_type = type;
}
}
重构前
public class Program
{
private static void Main(string[] args)
{
Employee emp = Employee.create();
Console.WriteLine(emp.getType());
Console.ReadKey();
}
} abstract class Employee
{
public static Employee create(int type)
{
switch (type)
{
case ENGINEER:
return new Engineer();
default:
return null;
}
}
public abstract int getType();
protected const int ENGINEER = ;
}
class Engineer : Employee
{
public override int getType()
{
return ENGINEER;
}
} 重构后
重构后
以状态模式和策略模式取代类型码(Replace Type Code with State/Strategy)
你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它。以状态对象取代类型码。
动机
如果“类型码的值在对象生命期中发生变化”或“其他原因使得宿主类不能被继承”,可以使用本重构。使用State模或Strategy模式
状态模式和策略模式非常相似,无论你选择其中哪一个,重构过程都是相同的。
如果打算在完成重构后,使用“以多态取代条件类型”来简化算法,那么选择策略模式比较合适。
如果打算搬移与状态相关的数据,而且你把新建对象视为一种变迁状态,就应该使用状态模式。
做法
1.使用“自封装字段”
2.新建一个类,根据类型码的用途为他重命名。
3.添加子类,每个子类对应一种状态码。比起逐一添加,一次性加入所有必要的子类可能更简单些。
4.父类中建立一个抽象的查询函数,用以返回类型码。每个子类重写并返回确切的类型码
5.编译
6.源类中建立一个字段,用以保存新建的状态对象。
7.调整源类中负责查询类型码的函数,将查询动作转发给状态对象。
8.调整源类中为类型码设值的函数,将一个恰当的状态对象给子类赋值给“保存对象的字段”
9.测试
范例
对象Employee的类型码是可变的。因为Employee可以又员工进阶为经理。所以不能用继承来处理类型码
class Employee
{
private int _type, _monthlySalary, _commission, _bonus;
public const int ENGINEER = ;
public const int SALESMAN = ;
Employee(int type)
{
_type = type;
}
int payAmount()
{
switch (_type)
{
case ENGINEER:
return _monthlySalary;
case SALESMAN:
return _monthlySalary + _commission;
default:
throw new ArgumentNullException(_type + ",不是有效的类型");
}
}
}
重构前
class Employee
{
private int _monthlySalary, _commission, _bonus;
private EmployeeType _type;
public int Type
{
get
{
return _type.getTypeCode();
}
set
{
_type = EmployeeType.newType(value);
}
} Employee(int type)
{
Type = type;
}
int payAmount()
{
switch (Type)
{
case EmployeeType.ENGINEER:
return _monthlySalary;
case EmployeeType.SALESMAN:
return _monthlySalary + _commission;
default:
throw new ArgumentNullException(Type + ",不是有效的类型");
}
}
} abstract class EmployeeType
{
public abstract int getTypeCode();
public const int ENGINEER = ;
public const int SALESMAN = ;
public static EmployeeType newType(int code)
{
switch (code)
{
case ENGINEER:
return new Engineer();
case SALESMAN:
return new Salesman();
default:
throw new ArgumentNullException(code + "-没有对应参数");
}
}
}
class Engineer : EmployeeType
{
public override int getTypeCode()
{
return ENGINEER;
}
}
class Salesman : EmployeeType
{
public override int getTypeCode()
{
return SALESMAN;
}
}
重构后
以字段取代子类(Replace Subclass with Fields)
你的各个子类的唯一差别只在“返回常量数据”的函数身上。修改这些函数,使他们返回父类中的某个字段,然后销毁子类。
动机
建立子类的目的,是为了增加新特性或变化其行为。但如果子类中只有常量函数(返回写死的编码),实在是没有存在价值。
可以在父类中设计一个与常量函数返回值相应的字段,从而完全去除这样的子类。就可以避免因继承而带来的额外复杂性。
做法
1.对所有子类使用“以工厂函数取代构造函数”
2.如果有任何代码引用子类,修改为引用父类。
3.针对每一个常量函数,声明一个readonly字段。
4.为父类声明一个protected构造函数,用以初始化这些新增字段。
5.修改子类构造函数,使他调用父类新增构造函数
6.测试
7.在超类中实现所有常量函数,令他们返回相应字段值,然后将该函数从子类中删掉。
8.没删掉一个常量函数,测试
9.使用“内链函数”将子类构造函数内链到超类工厂函数中
10.测试
11.将子类删掉
12.重复9,10,11步骤,直到所有子类都被删除
范例
使用Person代表人,针对性别建立一个子类:Male表示男人,Female表示女人,常量函数返回Code
abstract class Person {
public abstract bool isMale();
public abstract char getCode();
}
class Male : Person
{
public override char getCode()
{
return 'M';
} public override bool isMale()
{
return true;
}
}
class Female : Person
{
public override char getCode()
{
return 'F';
} public override bool isMale()
{
return false;
}
}
重构前
这两个子类之间唯一的区别就是:以不同的方式实现了getCode,返回硬编码常量
class Person
{
private bool _isMale;
private char _code;
protected Person(bool isMale, char code)
{
_isMale = isMale;
_code = code;
}
public static Person createMale()
{
return new Person(true, 'M');
}
public static Person createFemale()
{
return new Person(false, 'F');
}
}
重构后
简化条件表达式
条件逻辑有可能十分复杂,因此此章节的重构手法,是专门简化条件表达式的。核心重构是“分解条件表达式”,可将一个复杂的条件逻辑分成若干个小块。
这项重构很重要,因为它使得“分支逻辑”和“操作细节”分离。
如果发现代码中有多处测试相同的结果,应该实施“合并条件表达式”,如果条件代码中有任何重复,可以使用“合并重复的条件片段”将重复成分去掉。
如果坚持“单一出口”原则,使用“以谓语取代嵌套条件表达式”标示出特殊情况,并使用“移除控制标记”
一旦出现switch语句,就应该考虑运用“以多态取代条件表达式”。多态还有一种十分有用的用途:通过“引入NULL对象”去除null值的校验。
分解条件表达式(Decompose Conditional)
你有一个复杂的条件(if-then-else)语句,从if\then\else三个段落中分别提炼出独立函数。
动机
复杂的条件逻辑是最常导致复杂度上升的地点之一。必须编写代码来检查不同的条件分支,根据不同的分支做不同的事。
然后就会得到一个相当长的函数,大型函数自身就会使代码的可读性下降,而条件逻辑则会使代码更难阅读。
你可以将它分解为多个独立函数,根据每个小块代码的用途,为分解而得的新函数命名,并将源函数中对应的代码改为调用新增函数。
对于条件逻辑,将每个分支条件分解成新函数还可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。
做法
1.将if段落提炼出来,构成一个独立函数
2.将then段落和else段落都提炼出来,各自构成一个独立函数。
3.如果发现嵌套的条件逻辑,先观察是否可以使用“以谓语取代嵌套条件表达式”,如果不行才开始分解其中的每个条件。
范例
class Commodity
{
public DateTime date;
public DateTime SUMMER_START, SUMMER_END;
public double charge, quantity, _winterRate,_winterServiceCharge,_summerRate;
public Commodity() {
if (date < SUMMER_START || date > SUMMER_END)
{
charge = quantity * _winterRate + _winterServiceCharge;
}
else
{
charge = quantity * _summerRate;
}
}
}
重构前
class Commodity
{
public DateTime date;
public DateTime SUMMER_START, SUMMER_END;
public double charge, quantity, _winterRate, _winterServiceCharge, _summerRate;
public Commodity()
{
if (notSummer(date))
charge = winterCharge();
else
charge = summerCharge();
} private double summerCharge()
{
return quantity * _summerRate;
} private double winterCharge()
{
return quantity * _winterRate + _winterServiceCharge;
} private bool notSummer(DateTime date)
{
return date < SUMMER_START || date > SUMMER_END;
}
}
重构后
合并条件表达式(Consolidate Conditional Expression)
你有一系列条件测试,都得到相同结果。将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。
动机
有时你会发现一串条件检查,最终行为却一致。如果发现这种情况,就应该使用“或和与”将他们合并为一个条件表达式
之所以要合并,有两个原因
1.合并后的条件代码会告诉你“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”,从而使用以更清晰
2.这些重构往往可以为你使用“提炼函数”做好准备
不要合并的理由:如果你认为这些检查的确彼此独立,的确不应该被视为同一次检查,那么久不要使用。
做法
1.确定这些条件语句都没有副作用
2.使用适当的逻辑操作符,将一系列相关条件表达式合并为一个。
3.编译,测试
4.对合并后的条件表达式实施“提炼函数”
范例一 使用逻辑或
double disabilityAmount() {
if (_seniority < ) return ;
if (_monthsDisabled > ) return ;
if (_isPartTime) return ;
return ;
}
重构前
double disabilityAmount() {
if (isNotEligibleForDisability()) return ;
return ;
}
bool isNotEligibleForDisability() {
return (_seniority < ) || (_monthsDisabled > ) || (_isPartTime);
}
重构后
范例二 使用逻辑与
if (onVacation())
if (lengthOfService() > )
return ;
return ;
重构前
return (onVacation() && lengthOfService() > ) ? : ;
重构后
合并重复的条件片段(Consolidate Duplicate Conditional Fragments)
在条件表达式的每个分支上有着相同的一段代码,将这段重复代码搬移到条件表达式之外
动机
表明那些东西随条件的变化而变化,那些东西保持不变
略(条件中,相同代码放外面)
移除控制标记(Remove Control Flag)
在一系列布尔表达式中,某个变量带有“控制标记”的作用,以break语句或return语句取代控制标记。
动机
条件语句真正的用途会清晰很多。
略
以谓语取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)
函数中的条件逻辑使人难以看清正常的执行路径。使用谓语表现所有特殊情况。
动机
条件表达式通常有两种表现形式:1.所有分支都属于正常行为。2.只有一种是正常行为,其他都是不常见的情况。
如果两条分支都是正常行为,就是用if..else..的条件表达式,如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。
各个分支有同样的重要性,谓语句则不同,告诉阅读者“这种情况很罕见”
做法
1.对于某个检查条件,放进一个谓语句,要不就从函数中返回,要不就抛出异常
2.每次将条件检查替换成谓语句后,编译并测试,如果结果相同使用“合并条件表达式”
范例
一个薪酬系统,以特殊规则处理死亡员工、驻外员工、退休员工
double getPayAmount()
{
double result;
if (_isDead)
result = deadAmount();
else {
if (_isSeparated)
result = separatedAmount();
else {
if (_isRetired)
{
result = retiredAmount();
}
else {
result = normalPayAmount();
}
}
}
return result;
}
重构前
double getPayAmount()
{
if (_isDead) return deadAmount();
if (_isSeparated) return separatedAmount();
if (_isRetired) return retiredAmount();
return normalPayAmount();
}
重构后
以多态取代条件表达式(Replace Conditional with Polymorphism)
你手上有个条件表达式,它根据对象类型的不同而选择不同的行为。将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。
动机
多态最根本的好处是:需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显的条件表达式。
如果同一组条件表达式在程序许多地方出现,那么使用多态的收益是最大的。
使用表达式,如果想添加一种新类型,就必须查找并更新所有条件表达式。
改用多态,只需要建立一个新的子类,并且在其中提供适当的函数就行了。
做法
使用本重构之前,必须现有一个继承结构。有两种选择“子类取代类型码”和“状态/策略模式取代类型码”。
如果若干switch语句针对的是同一个类型码,你只需针对这个类型码建立一个继承结构就行了。
1.如果条件表达式是更大函数中的一部分,首先对条件表达式进行分析,然后“提炼函数”将它提炼到一个独立函数中。
2.使用“搬移函数”将条件表达式放置到继承结构的顶端
3.任选一个子类,其中建立函数,使之覆写父类中容纳条件表达式的那个函数。
4.测试
5.父类中删掉条件表达式内被复制的分支
6.测试
7.针对条件表达式的每个分支,重复上述过程,直到所有分支都被移到子类内的函数为止。
8.将父类中容纳条件表达式的函数声明为抽象函数
范例
使用“以状态/策略取代类型码”重构后的例子
class Employee
{
private EmployeeType _type;
public int Type
{
get
{
return _type.getTypeCode();
}
set
{
_type = EmployeeType.newType(value);
}
}
public int MonthlySalary { get; set; }
public int Commission { get; set; }
public Employee(int type)
{
Type = type;
}
public int payAmount()
{
return _type.payAmount(this);
}
} abstract class EmployeeType
{
public abstract int getTypeCode();
public abstract int payAmount(Employee emp);
public const int ENGINEER = ;
public const int SALESMAN = ;
public static EmployeeType newType(int code)
{
switch (code)
{
case ENGINEER:
return new Engineer();
case SALESMAN:
return new Salesman();
default:
throw new ArgumentNullException(code + "-没有对应参数");
}
}
}
class Engineer : EmployeeType
{
public override int getTypeCode()
{
return ENGINEER;
} public override int payAmount(Employee emp)
{
return emp.MonthlySalary;
}
}
class Salesman : EmployeeType
{
public override int getTypeCode()
{
return SALESMAN;
} public override int payAmount(Employee emp)
{
return emp.MonthlySalary + emp.Commission;
}
}
重构后
简化函数调用
本章将介绍几个使接口变得更简洁易用的重构手法。
只要你能理解一段程序的功能,就应该大胆的使用“函数改名”,将你所知道的东西传递给其他人。
函数参数在接口中扮演十分重要的角色。“添加参数”和“移除参数”都是很常见的重构手法。
如果来自同一对象的多个值被当作参数传递,可以运用“保持对象完整性”替换为单一对象。
如果此前不存在这样的一个对象,你可以运用“引入参数对象”。如果函数参数来自该函数可获取的一个对象,使用“以函数取代参数”。
如果某些参数被用来在条件表达式中做选择依据,可以实施“以异常取代错误码”,还可以使用“令函数携带参数”
如果接口暴露了过多的细节,使用“隐藏函数”和“移除设置函数”将他们隐藏起来。
使用“以工厂取代构造函数”避免必须知道对象所属的类。
使用“以异常取代错误码”来运用新的异常特性。有时候异常也并不是最合适的选择,应该实施“以测试取代异常”先进行测试。
函数改名(Rename Method)
函数的名称未能揭示函数的用途,就需要修改函数名称。
动机
将复杂的处理过程分解成小函数。但如果做得不好,会让你弄不起这些小函数各自的用途。关键就在于给函数起一个好名称。
首先考虑应该给这个函数写上一句怎样的注释,然后想办法将注释编程函数名称。
做法
1.检查函数签名是否被父类或子类实现过
2.声明一个新函数,将它命名成你想要的新名词。复制代码进行调整
3.编译
4.修改旧函数,令他将调用转发给新函数
5.测试
6.找出旧函数的所有被引用点,修改他们,令他们改而引用新函数。
7.删除旧函数
8.编译,测试
范例
略
以明确函数取代参数(Replace Parameter with Explicit Methods)
你有一个函数,其中完全取决于参数值而采取不同行为,针对该参数的每一个可能值,建立一个独立函数。
动机
如果某个参数有多种可能的值,而函数内又以条件表达式检查这些参数值,并根据不同参数值做出不同的行为,就应该使用本重构
做法
1.针对参数的每一种可能值,新建一个明确函数
2.修改条件表达式的每一个分支,使其调用合适的新函数。
3.修改每个分支后,编译并测试
4.修改源函数的每一个被调用点,改而调用上述的某个合适的新函数
5.编译测试
6.所有调用端都修改完毕后,删除原函数。
范例
class Employee
{
static int ENGINEER = ;
static int SALESMAN = ;
static Employee create(int type) {
switch (type)
{
case ENGINEER:
return new Engineer();
case SALESMAN:
return new Salesman();
default:
throw new ArgumentNullException();
}
}
}
重构前
class Employee
{
static int ENGINEER = ;
static int SALESMAN = ;
static Employee create(int type) {
switch (type)
{
case ENGINEER:
return createEngineer() ;
case SALESMAN:
return createSalesman();
default:
throw new ArgumentNullException();
}
}
static Employee createEngineer() { return new Engineer(); }
static Employee createSalesman() { return new Salesman(); }
}
重构后
保持对象完整(Preserve Whole Object)
你从某一个对象中取出若干值,将它们作为某一次函数调用时的参数。改为传递整个对象
动机
万一将来被调用函数需要新的数据项,你就必须查找并修改对此函数的所有调用。
以函数取代参数(Replace Parameter with Methods)
对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。让参数接受者去除该项参数,并直接调用前一个函数。
动机
如果函数可以通过其他途径获得参数值,那么他就不应该通过参数取的值。过长的参数列会增加程序阅读者的理解难度。
1.看看参数接收端是否可以通过与调用端相同的计算来取得参数值。如果调用端通过其所属对象内部的另一个函数来计算参数,并在计算过程中未曾引用调用端的其他参数,
那么就可以将这个计算过程转移到被调用端内,从而去除该项参数。如果参数值的计算依赖于某个调用端的参数,那么久无法去除参数。
做法
1.将参数的计算过程提炼到一个独立函数中。
2.将函数本体内引用该参数的地方改为调用新建的函数。
3.每次替换后,修改并测试
4.全部替换完成后,使用“移除参数”将该参数去掉。
范例
public double getPrice() {
int basePrice = _quantity * _itemPrice;
int discountLevel;
if (_quantity > ) discountLevel = ;
else discountLevel = ;
double finalPrice = discountedPrice(basePrice, discountLevel);
return finalPrice;
}
private double discountedPrice(int basePrice, int discountLevel) {
if (discountLevel == ) return basePrice * 0.01;
else return basePrice * 0.05;
}
重构前
public double getPrice() {
if (getDiscountLevel() == ) return getBasePrice() * 0.01;
else return getBasePrice() * 0.05;
}
private double getBasePrice() {
return _quantity * _itemPrice;
}
private int getDiscountLevel() {
if (_quantity > ) return ;
else return ;
}
重构后
引入参数对象(Introduce Parameter Object)
某些参数总是很自然的同时出现,以一个对象取代这些参数。
动机
你经常会看到特定的一组参数总是一起被传递,可能有好几个函数都使用这一组参数。这样的参数,就是所谓的“数据泥团”。
可以运用一个对象包装所有这些数据,再议该对象取代他们。哪怕只是为了把数据组织在一起,也是值得的。
做法
1.新建一个类,用以表现你想替换的一组参数。将这个类设为不可变
2.编译
3.针对所有使用该组参数的所有函数,实施“添加参数”,传入上述新建类的实例对象,并将此参数值设为null
4.对于“数据泥团”中的每一项,从函数签名中移除之,并修改调用端和函数本体,令他们都改而通过新的参数去的该值。
5.每去除一个参数,编译并测试。
6.将原先的参数全部去除之后,观察有无适当函数可用“搬移函数”搬移到参数对象之中
范例
日期范围就可以引入参数对象
class DateRange
{
private readonly DateTime _start, _end;
internal DateRange(DateTime start, DateTime end)
{
_start = start;
_end = end;
}
DateTime getStart() {
return _start;
}
DateTime getEnd() {
return _end;
}
}
时间范围
移除设置函数(Remove Setting Method)
类中的某个字段应该在对象创建时被设值,然后就不在改变。去掉该字段的所有设置函数。
动机
这样会让你的意图更加清晰,并且可以排除其值被修改的可能性,这种可能性往往非常大
略
隐藏函数(Hide Method)
有一个函数,从来没有被其他任何类用到。将这个函数修改为private。
动机
当你面对一个过于丰富、提供了过多行为的接口时,就值得将非必要的取值函数和设置函数隐藏起来。
做法
1.经常检查有没有可能降低某个函数的可见度
2.尽可能降低所有函数的可见度。
3.每完成一组函数的隐藏之后,编译并测试
略
以工厂函数取代构造函数(Replace Constructor with Factory Method)
你希望在创建对象时不仅仅是做简单的建构动作。将构造函数替换为工厂函数。
动机
在派生子类的过程中以工厂函数取代类型码。可能需要根据类型码创建相应的对象,那些子类也是根据类型码来创建。
然而由于构造函数只能返回单一类型对象,因此你需要将构造函数替换为工厂函数。
做法
1.新建一个工厂函数,让它调用现有的构造函数。
2.将调用构造函数的代码改为调用工厂函数。
3.每次替换后,编译并测试
4.将构造函数声明为private。
5.编译
范例一 根据整数类型码创建对象
class Employee
{
private int _type;
public const int ENGINEER = ;
public const int SALESMAN = ;
public Employee(int type)
{
_type = type;
}
}
重构前
class Employee
{
private int _type;
public const int ENGINEER = ;
public const int SALESMAN = ;
private Employee(int type)
{
_type = type;
}
public static Employee create(int type)
{
return new Employee(type);
}
}
重构后
以异常取代错误码(Replace Error Code with Exception)
某个函数返回一个特定的代码,用以表示某种错误情况。改用异常。
动机
使用异常可以清楚的将“普通程序”和“错误处理”分开,这使得程序更容易理解。
略
处理继承关系
“字段上移”和“函数上移”都用于将特性向继承体系的上端移动,“字段下移”和“函数下移”则将特性向继承体系的下端移动。
构造函数比较难以向上拉到,因此专门有一个“构造函数本体上移”处理它。
“提炼子类”,“提炼父类”,“提炼接口”都是在继承体系的不同位置构造出新元素。如果想在类型系统中表示一小部分函数,“提炼接口”特别有用。
如果发现继承体系中某些类没有存在必要,可以使用“折叠继承体系”将他们移除
“以委托取代继承”可以帮助你把继承改为委托。有时候会想反向修改,就可以使用“以继承取代委托”
字段上移(Pull Up Field)
两个子类拥有相同的字段,将该字段移至父类。
略
函数上移(Pull Up Method)
有些函数,在各个子类中产生完全相同的结果。将该函数移至父类。
略
构造函数本体上移(Pull Up Constructor Body)
你在各个子类中拥有一些构造函数,他们的本体几乎完全一致。在父类中新建一个构造函数,并在子类构造函数中调用它。
动机
如果看见各个子类中的构造函数有共同行为,这时候需要在父类中提供一个构造函数,然后让子类都调用它
:base(this)
函数下移(Push Down Method)
父类中的某个函数只与部分子类有关,将这个函数移到相关的那些子类去。
略
字段下移(Push Down Field)
超类中的某个字段只被部分子类用到,将这个字段移到需要它的那些子类去。
略
提炼子类(Extract subclass)
类中的某些特性只被某些实例用到。新建一个子类,将上面所说的那一部分特性移到子类中。
略
提炼父类(Extract Superclass)
两个类有相似特性。为这两个类建立一个父类,将相同特性移至父类。
略
提炼接口(Extract Interface)
若干客户使用类接口中的同一子集,或者两个类的接口有部分相同。将相同的子集提炼到一个独立接口中。
略
折叠继承体系(Collapse Hierarchy)
超类和子类之间无太大区别,将他们合为一体。
略
塑造模板函数(Form Template Method)
你有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同。
将这些操作分别放进独立函数中,并保持他们都有相同的签名,于是源函数也就变得相同了。然后将源函数上移至父类。
略
以委托取代继承(Replace Inheritance with Delegation)
某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据。
在子类中建立一个字段用以保存超类;调整子类函数,令他改而委托超类;然后去掉两者之间的继承关系
略
以继承取代委托(Replace Delegation with Inheritance)
你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数。让委托类继承受托类。
略