重构改善既有代码的设计
重构改善既有代码的设计
第二章 重构原则
重构
- 重构[名词]:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
- 重构[动词]:使用一系列重构手法,在不改变软件可古纳差行为的前提下,调整其结构。
重构的目的
- 使软件更容易被理解和修改。你可以在软件内部做很多修改,但必须对软件可观察的外部行只造成很小变化,或甚至不造成变化。与之形成对比的事性能优化。和重构一样,性能优化通常不会改变组件的行为(除了执行速度),只会改变其内部结构。但两者出发点不同:性能优化往往使代码较难理解,但为了得到所需要的性能你不得不那么做。
为什么要重构
- 重构改进软件设计
-
- 烂代码、重复性、程序结构复杂紊乱、维护困难
- 重构使软件更容易理解
-
- 第二个程序员接管、修改大于理解、重新查阅代码
- 重构帮助找到bug
-
- 调试、强健的代码
- 重构提高编程速度
-
- 快速编程
何时重构
-
三次法则[事不过三,三则重构]
-
- 第一次做某件事时只管去做
-
- 第二次做类似的事会产生反感
-
- 第三次再做类似的事,你就应该重构
-
添加新功能时重构
-
- 整理代码结构,便于理解、原有代码设计无法帮助我轻松添加所需的特性
-
修补错误时重构
-
- 代码可读性
-
复审代码时重构
-
- 让别人更好的理解自己的代码、结对编程
为什么程序会难以理解
- 难以阅读的程序,难以修改
- 逻辑重复的程序,难以修改
- 添加新行为时需要修改已有代码的程序,难以修改
- 带复杂条件逻辑的程序,难以修改
因此我们希望的程序是
- 容易阅读
- 所有逻辑都只在唯一地点指定
- 新的改动不会危及现有行为
- 尽可能简单表达条件逻辑
修改接口
- 关于对象,另一件重要的事是:他们允许你分开修改软件模块饿的实现和接口。你可以安全地修改对象内部实现而不影响他人,但对于接口要特别谨慎--如果接口被修改了,任何事都可能发生。
如何修改已发布接口
- 让就接口调用新接口。当你要修改某个函数名称时,请留下旧函数,让它调用新函数。千万不要复制函数的实现,那会让你陷入重复代码的泥沼中难以自拔。你还应该使用Java提供的deprecation(不建议使用)设施,将旧接口标记为deprecated。这么一来你的调用者旧会注意到它了。
- 不要过早发布接口。请修改你的代码所有权政策,使重构更顺畅。
难以重构的思路
- 先想象重构的情况。考虑候选设计方案时,我会问自己:将某个设计重构为另一个设计的难度有多大?如果看上去很简单,我就不必太担心选择是否得当,于是我就会选择最简单的设计,哪怕它不能覆盖所有潜在需求也没关系。但如果预先看不到简单的重构方法,我就会设计上投入更多力气。
何时不该重构
- 现有代码根本不能正常运行。
- 重构的代码太混乱,还不如重写一个来的简单。
- 一个折中的办法就是:将“大块头软件”重构为封装良好的小型组件。然后你就可以逐一对组件做出“重构或重建”的决定。
- 项目已近最后限期,避免重构。
重构与设计
- 有一种观点认为:重构可以取代预先设计。这意思是你根本不必做任何涉及,只管按照最初想法开始编码,让代码有效运作,然后再将它重构成型。事实上这种办法真的可行。的确有人这么做,最后获得设计良好的软件。
- 有些编程是使用CRC卡或类似的东西来检验各种不同想法,然后才得到第一个可被接受的解决方案,然后才能开始编码,然后才能重构。关键在于:重构改变了预先设计的角色。
CRC卡
第三章 代码的坏味道
重读代码[Duplicated Code]
- 重复的函数
-
- 解决:抽离成一个函数
- 兄弟类中出现相同表达式
-
- 解决:把相同的函数推向父类
- 毫不相关的两个类
-
- 解决:使用抽象类,两个类同继承抽象类
过长函数[Long Method]
分解函数的原则
- 每当感觉需要以注释说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手段)命名。我们可以对一组甚至短短一行代码做这件事。哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,我们也该毫不犹豫地name做。关键不在于函数的长度,而在于函数“做了什么”和“如何做”之间的语义距离。
- 百分之九十九的场合里,要把函数变小,只要使用Extract Method。找到函数中适合集中在一起的部分,将它们提炼出来形成一个函数。
如何确定该提炼哪一段
- 寻找注释。它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有注释,就是在提醒你可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得被提炼出来。
- 条件表达式和循环常常也是提炼的信号。你应将循环和其内的代码提炼到一个独立函数中。
过大的类
- 单个类做太多的事情,其内往往就会出现太多实例变量。一旦如此,重复的代码也就会接踵而至。
- 可以使用抽象类将几个变量一起提炼到这个新类中。通常如果类内的数个变量有相同的前缀或字尾,这就意味有机会把它们提炼到某个组件内。如果这个组件适合作为一个子类,你会发现抽象子类往往比较简单。
“太多实例变量”
- 解决:最简单的是把多余的东西分解与内部类。如果有5个“百行函数”,它们之中很多代码都相同,name或许你可以把它们编程5个“十行函数”和提炼出来的“双行函数”。
“拥有太多实例变量”
- 先确定客户端如何使用它们,然后运用抽象接口为每一种使用方式提炼出一个接口。
过长参数列[Long Parameter List]
发散式变化[Divergent Change]
- 如果当你看到一个类,“如果新加入一个数据库,我必须修改这三个函数;如果新出现一种金融工具,我必须修改4个函数”。那么此时也许将这个对象分成两个会更好,这么一来每个对象就可以只因一种变化而需要修改。
- 运用抽象类将它们提炼到了另一个类中。
霰弹式修改
- 每遇到某种变化,你都必须在许多不同的类内做出许多小修改。
- 解决:把所有需要修改的代码放到同一个类。如果眼下没有合适的类可以安置这些代码,就创造一个。通常可以运用Inline Class把一系列相关行为放到同一个类。这可能造成少量的重复变更动作,也可以轻易处理它。
依恋情节[Feature Envy]
一个函数往往会用到几个类的功能,那么它究竟应该被置于何处呢
- 判断哪个类拥有最多被此函数使用的数据,然后把这个函数和那些数据摆一起。
- 如果先以Extract Method将这个函数分解成数个小函数并分别置放雨不同地点,上述步骤也就比较容易完成了。
- 最根本的原则是:将总是一起变化的东西放在一块儿。数据和引用这些数据的行为总是一起变化的,但也有例外。如果例外出现,我们就搬移那些行为,保持变化只在一地发生。
数据泥团[Data Clumps]
常常看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象
- 首先找出这些数据以字段形式出现的地方,运用Extra Class将它们提炼到一个独立对象中。
- 然后将注意力转移到函数签名上,运用Introduce Parameter Object或Preserve Whole Object为它减肥。这样做的直接好处是可以将很多参数列缩短,简化函数调用。
- 一个好的评判方法是:删除众多数据中的一项。这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是个明确暗号:你应该为它们产生一个新对象。
基本类型偏执[Primitive Obsession]
大多数编程环境都有两种数据
-
结构类型允许你将数据组织成有意义的形式。
-
基本类型则是构成结构类型的积木块。
-
如果你有一组应该总是被放在一起的字段,可运用Extract Class。
-
如果你在参数列中看到基本型数据,不妨试试Introduce Parameter Object。
-
如果你发现自己正从数组中挑选数据,可运用Replace Array with Object。
switch惊悚现身[Switch Statements]
面向对象程序的一个最明显的特征就是:少用switch(或case)语句。从本质上说,switch语句的问题在于重复。你常会发现同样的switch语句散步于不同点。如果要为它添加一个新的case子句,就必须找到所有switch语句并修改它们,面向对象中的多态概念可以为此带来优雅的解决方法。
- 一旦看到switch就应该考虑以多态来替换它。
平行继承体系[Parallel Inheritance Hierarchies]
每当你为这个类增加一个子类,必须为另一个类增加一个子类。如果你发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同,便是闻到了坏的味道。
- 让一个继承体系的实例引用到另一个继承体系的使用。
冗赘类[Lazy Class]
如果一个雷的所得不值其身价,它就应该消失。
夸夸其谈未来性[Speculative Generality]
- 如果所有装置都会被用到,name就值得那么做;如果用不到,就不值得。用不上的装置只会挡住你的路,所以,把他搬开吧。
- 如果你的某个抽象类其实没有太大作用,请运用Collapse Hierarchy。
- 不必要的委托可运用Inline Class除掉。
- 如果某些函数参数未被使用,可以使用Remove Parameter。
- 如果函数名称带有多余的抽象意味,应该对他实施Rename Method,让它现实一些。
- 如果函数或类的唯一用户是测试用例,可以丢弃。但如果它们的用途是帮助测试用例检测正当功能,当然必须刀下留人。
令人迷惑的暂时字段[Temporary Field]
有时会有这样的对象:其内某个实例变量仅为某种特定情况而设。这代码令人不易理解,因为你通常认为对象在有所时候都需要它的所有变量。
- 将这些只在使用该算法时才有效,其他情况下只会让人迷惑,可以使用Extract Class将这些变量和其相关函数提炼到一个独立类中,提炼后的新对象将是一个函数对象。
过度耦合的消息链[Message Chains]
如果你看到用户向一个对象请求另一个对象,然后再向后者请求一个对象,然后再请求另一个对象。。。这就是消息链。实际代码中你可能会看到一长串的getThis()或者一长串临时变量。采取这种方式,意味客户代码将于查找过程中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。
- 使用Hide Delegate。你可以在消息链的不同位置进行这种重构手法。
- 先观察消息链最终对象得到的对象是用来干什么的,看看能否以Extract Method把使用该对象的代码提炼到另一个独立函数中,再运用Move Method把这个函数推入消息链。
- 如果这个消息链上的某个对象有多位客户端航行此航线的剩余部分,就加一个函数来做这件事。
中间人[Middle Man]
看到某个类接口有一半的函数都委托给其他类,这样就是过度运用。
狎昵关系[Inappropriate Intimacy]
两个类过于亲密。过于狎昵的类必须拆散,要严守清规。
- 使用Move Method和Move Field帮他们划清界限,减少狎昵行径。
- 运用Change Bidirectional Association to Unidirectional 让其中一个类对另一个类斩断情丝。
- 运用Extract Class把两者共同点提炼到一个安全点,让他们坦荡地使用这个新类。
- 运用Hide Delegate让另一个类来为他们传递相思情。
异曲同工的类[Alternative Classed with Different Interfaces]
如果两个函数做同一件事,却有着不同的签名,运用Rename Method根据它们的用途重新命名。但这往往不够,请反复运用Move Method将某些行为移入类,直到两者的协议一致为止。如果必须重复而赘余地移入代码才能完成,运用Extract Superclass为自己赎点罪。
不完美的库类[Incomplete Library Class]
- 如果你只想修改库类的一两个函数,可以运用Introduce Foreign Method。
- 如果想添加一大推额外的行为,可以运用Introduce Local Extension。
纯稚的数据类[Data Class]
所谓Data Class是,它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。
- 运用Encapsulate Field将它们封装起来。
- 运用Encapsulate Collection把它们封装起来。
- 对于不被其他类修改的字段,运用Remove Setting Method。
- 找出取值/设置函数被其他类运用到的地点,以Move Method把那些调用行为搬移到Data Class中。
- 无法搬移的函数,运用Extract Method产生一个可被搬移的函数。
被拒绝的遗赠[Refused Bequest]
子类应该继承超类的函数和数据。如果它们不想或不需要继承,又该怎么办?它们得到了所有礼物,却只挑几样。按传统说法,意味着继承体系设计错误。
- 你需要为这个子类新建一个兄弟类,再运用Push Down Method和Push Down Field把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。你常会听到这样的建议:所有超类都应该是抽象的。
- 如果子类复用了超类的行为(实现),却又不愿意支持超类的接口。运用Replace Inheritance with Delegation来达到目的。
过多的注释[Comments]
当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都是变得多余。
如果你不知道该做什么,这才是注释的良好运用时机。除了用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里面写下自己“为什么做某某事”。这类信息可以帮助将来的维护者,尤其那些健忘的家伙。
- 如果你需要注释来解释一块代码做了什么,试试Extract Method。
- 如果函数已经提炼出来,但还是需要注释来解析其行为,试试Rename Method。
- 如果你需要注释说明某些系统的需求规格,试试Introduce Assertion。
第四章构筑测试体系
频繁地运行测试。每次编译请把测试考虑进去-每天至少执行每个测试一次。
每当你收到bug报告,请先写一个单元测试来暴露bug。
编写未万重山的测试并实行运行,好过对完美测试的无尽等待。
考虑可能出错的边界条件,把测试火力集中在那。
- “寻找边界条件”,包括寻找特殊的、可能导致测试失败的情况。对于文件相关测试,空文件是个不错的边界条件。
当事情被认为应该出错时,别忘了检查是否跑出了预期的异常。
几条测试规则建议
- 当测试数量达到一定程度之后,继续增加测试带来的效益就会呈现递减态势,而非持续递增。
- 如果试图编写太多测试,你也可能因为工作量太大而气馁,最后什么都写不成。
- 你应该把测试集中在可能出错的地方。
- 观察代码,看哪儿变得复杂;观察函数,思考那些地方可能出错。
- 是的,你的测试不可能找出所有bug,但一旦进行重构,你可以更好地理解整个程序,从而找到更多bug。
不要因为测试无法捕捉所有bug就不写测试,因为测试的确可以捕捉到大多数bug。
- 测试代码和产品代码之间的区别:你可以放心地复制、编写测试代码。
第五章 重构列表
重构的记录格式
- 首先是名称。
- 名称之后是一个简短概要。
- 动机为你介绍“为什么需要这个重构”和“什么情况下不该使用这个重构”。
- 做法简明扼要地一步一步介绍如何进行此一重构。
- 范例以一个十分简单的例子说明此重构手法如何运行。
第六章 重新组织函数
Extract Method[提炼函数]
将这段代码放入一个独立函数中,并让函数名解释该函数的用途。
动机、好处
- 简短命名良好的函数粒度都很小,那么函数被复用的机会就更大。
- 这会使高层函数读起来就像一系列的解释。
- 如果函数都是细粒度,那么函数的复写也会更容易些。
做法
- 创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是它“怎么样做”命名)。
- 将提炼出来的代码从源函数复制到新建的目标函数中。
- 仔细检查提炼出来的代码,看看其中是否引用了“作用域限于源函数”的变量。
- 检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将他们声明为临时变量。
- 检查被提炼代码段,看看是否有任何局部变量的值被它改变。
- 将被体力啊代码段中需要读取的局部变量,当作参数传给目标函数。
- 处理完所有局部变量之后,进行编译。
- 在源函数中,将被提炼代码段替换为目标函数的调用。
- 编译,测试。
如果需要返回的变量不止一个
- 挑选另一块代码来提炼。
- 让每个函数都只返回一个值。
- 安排多个函数,用以返回多个值。
Inline Method [内联函数]
在函数调用点插入函数本体,然后移除该函数
int getRating(){
return (moreThanFiveLateDeliveries()) ? 2 : 1 ;
}
boolean moreThanFiveLateDeliveries(){
return _numberOfLateDeliveries > 5;
}
- 变成
int getRating(){
return (_numberOfLateDeliveries > 5) ? 2 : 1 ;
}
动机
- 使代码更清晰易读。
- 手上有一群组织不合理的函数,可以将它们都内联到一个大型函数中,再从中提炼出组织合理的小函数。
做法
- 检查函数,确定它不具多态性。
- 找出这个函数的所有调用点。
- 将这个函数的所有调用点都替换为函数本体。
- 编译,测试。
- 删除该函数的定义。
Inline Temp [内联临时变量]
场景
- 你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其它重构手法。
将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。
double basePrice = anOrder.basePrice();
return (basePrice > 100)
- 变成
return (anOrder.basePrice() > 100)
动机
- 你发现某个临时变量被赋予某个函数调用的返回值。
做法
- 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
- 如果这个临时变量并未被声明为final,那就将它声明为final,然后编译。
- 找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式。
- 每次修改后,编译并测试。
- 修改完所有引用点之后,删除该临时变量的声明和赋值语句。
- 编译,测试。
Replace Temp with Query [以查询取代临时变量]
场景
- 你的程序以一个临时变量保存某一个表达式的运行结果。
将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为新函数的调用。此后,新函数就可以被其他函数使用。
double basePrice = _quantity * _itemPrice;
if (basePrice > 100)
return basePrice * 0.95;
else
return basePrice * 0.98;
- 变成
if(basePrice() > 100)
return basePrice() * 0.95;
else
return basePrice() * 0.98;
动机
- 临时变量的问题在于:它们是暂时的,而且只能在所属函数内使用。由于临时变量只在所属函数内可见,所以它们只会驱使你写出更长的函数,因为只有这样你才能访问到需要的临时变量。
- 如果把临时变量替换成一个查询,那么同一个类中的所有函数都将可以获得这份信息。
- Relace Temp with Query往往是你运用Extract Method之前必不可少的一个步骤。局部变量会使代码难以被提炼,所以尽可能把它们替换为查询方式。
- 临时变量只被复制一次,或者复制给临时变量的表达式不受其他条件影响。
做法
- 找出只被赋值一次的临时变量。
- 将该临时变量生命为final。
- 编译。
- 将“对该临时变量赋值”之语句的等号右侧部分提炼到一个独立函数中。
- 编译,测试。
- 在该临时变量身上实施Inline Temp。
我们常常使用临时变量保存循环中的累加信息。在这种情况下,整个循环都可以被提炼为一个独立函数,这也使原本的函数可以减少掉几行扰人的循环逻辑。
Introduce Explaining Variable[引入解释性变量]
场景
- 一个复杂的表达式。
将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
if ((platform.toUpperCase().indexOf("MAC") > -1 ) && (browser.toUpperCase().indexOf("IE") > -1))
{
do something
}
- 变成
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
if ( isMacOs && isIEBrowser)
{
do something
}
动机
- 表达式有可能非常复杂而难以阅读。这种情况啊下,临时变量可以帮助你讲表达式分解为比较容易管理的形式。
- 将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。
- 在较长的算法中,可以运用临时变量来解释每一步运算的意义。
做法
- 声明一个final临时变量,将待分解之复杂表达式中的一部分动作运算结果复制给它。
- 将表达式中“运算结果”这一部分,替换为上述临时变量。
- 编译,测试。
- 重复上述过程,处理表达式其他部分。
Split Temporary Variable[分解临时变量]
场景
- 你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。
针对每次赋值,创造一个独立、对应的临时变量。
double temp = 2 * (_height + _width);
System.out.print(temp);
temp = _height * _width;
System.out.print(temp);
- 变成
final double perimeter = 2 * (_height + _width);
System.out.print(perimeter);
final double area = _height * _width;
System.out.print(area);
动机
- 临时变量应该只被赋值一次。如果他们被赋值超过一次,就意味着它们在函数中承担了一个以上的责任。
- 如果临时变量承担多个责任,它就应该被替换(分解)为多个临时变量,每个变量只承担一个责任。
- 同一个临时变量承担两件不同的事情,会令代码阅读者糊涂。
做法
- 在待分解临时变量的声明及第一次被赋值处,修改其名称。
- 将新的临时变量声明为final。
- 以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量。
- 在第二次赋值处,重新声明原先那个临时变量。
- 编译,测试。
- 逐次重复上述过程。
Remove Assignments to Parameters[移除对参数的赋值]
场景
- 代码对一个参数进行赋值
以一个临时变量取代该参数的位置。
int discount (int inputVal, int quantity, int yearToData) {
if (inputVal > 50) inputVal -=2;
}
- 变成
int discount (int inputVal, int quantity, int yearToData) {
result = inputVal;
if (inputVal > 50) result -=2;
}
做法
- 建立一个临时变量,把待处理的参数赋予它。
- 以“对参数的赋值”为界,将其后所有对此参数的引用点,全部替换为“对此临时变量的引用”。
- 修改赋值语句,使其改为对新建之临时变量赋值。
- 编译,测试。
还可以为参数加上关键词final,从而强制它遵循“不对参数赋值”这一惯例。
Replace Method with Method Object[以函数对象取代函数]
场景
- 你有一个大型函数,其中对局部变量的利用使你无法采用Extract Method。
将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。然后你可以在同一个对象将这个大型函数分解为多个小型函数。
动机
- 将所有局部变量变成函数对象的字段,然后你就可以对这个新对象使用Extract Method创造出新函数,从而将原本的大函数拆解变短。
做法
- 建立一个新类,根据待处理函数的用途,为这个类命名。
- 在新类中建立一个final字段,用以保存原先大型函数所在对象。我们将这个字段称为“源对象”。同时,针对原函数的每个临时变量和每个参数在新类中建立一个对应的字段保存之。
- 在新类中建立一个构造函数,接收源对象及原函数的所有参数作为参数。
- 在新类中建立一个compute()函数。
- 将原函数的代码复制到compute()函数中。如果需要调用源对象的任何函数,请通过源对象字段调用。
- 编译。
- 将旧函数的函数本体替换为这样一条语句:“创建上述新类的一个新对象,而后调用其中的compute()函数”。
Substitute Algorithm[替换算法]
场景
- 你想要把某个算法替换为另外个更清晰的算法。
将函数本体替换为另一个算法。
String foundPerson (String[] person) {
for (int i = 1; i < person.length; i++) {
if (people[i].equals("Don")) {
return "Don";
}
if (people[i].equals("John")) {
return "John";
}
if (people[i].equals("Kent")) {
return "Kent";
}
}
return "";
}
- 变成
String foundPerson (String[] people) {
List candidates = Arrays.asList(new String[] {"Don", "John", "Kent"});
for (int i = 1; i < people.length; i++) {
if (candidates.contains(people[i]))
return people[i];
}
return "";
}
动机
- 使用这项重构手法之前,请先确定自己已经尽可能分解了原先函数。替换一个巨大而复杂的算法是非常困难的,只有先将它分解为较为简单的小型函数。
做法
- 准备好另一个算法,让它通过变异。
- 针对现有测试,执行上述的新算法。如果结果与原本结果相同,重构结束。
- 古国测试结果不同于原先,在测试和调试过程中,以旧算法为比较参数标准。
第7章 在对象之间搬移特性
Move Method[搬移函数]
场景
- 你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。
在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。
动机
- 如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,我就会搬移函数。
- 通过这种手段,可以使系统中的类更简单,这些类最终也将更干净利落地实现系统交付的任务。
做法
- 检查源类中被源函数所使用的一切特性,考虑它们是否也该被搬移。
- 检查源类的子类和超类,看看是否有该函数的其他声明。
- 在目标类中声明这个函数。
- 将源函数的代码复制到目标函数中。调整后者,使其能在新家中正常运行。
- 编译目标类。
- 决定如何从源函数正确引用目标对象。
- 修改源函数,使之成为一个纯委托函数。
- 编译,测试。
- 决定是否删除源函数,或将它当做一个委托函数保留下来。
- 如果要移除源函数,请将源类中对源函数的所有调用,替换为目标函数的调用。
- 编译,测试。
Move Field[搬移字段]
场景
- 你的程序中,某个字段将其所驻类之外的另一个类更多地用到。
在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段。
动机
- 如果发现一个字段,在其所驻类之外的另一个类中有更多函数使用了它,我就会考虑搬移这个字段。
做法
- 如果字段的访问级别是public,使用Encapsulate Field将它封装起来。
- 编译,测试。
- 在目标类中建立与源字段相同的字段,并同时建立相应的设置/取值函数。
- 编译目标类。
- 决定如何在源对象中引用目标对象。
- 删除源字段。
- 将所有对源字段的引用替换为对某个目标函数的调用。
- 编译,测试。
Extract Class[提炼类]
场景
- 某个类做了应该由两个类做的事。
建立一个新类,将相关的字段和函数从旧类搬移到新类。
动机
- 一个类应该是一个清楚的抽象,处理一些明确地责任。
- 另一个往往在开发后期出现的信号是类的子类化方式。
动机
- 决定如何分解类所负的责任。
- 建立一个新类,用以表现从旧类中分离出来的责任。
- 建立“从旧类访问新类”的连接关系。
- 对于你想搬移的每一个字段,运动Move Field搬移之。
- 每次搬移后,编译,测试。
- 使用Move Method将必要函数搬移到新类。先搬移较底层函数(也就是“被其他函数调用”多于“调用其他函数”),在搬移较高层函数。
- 每次搬移之后,编译,测试。
- 检查,精简每个类的接口。
- 决定是否公开新类。如果你的确需要公开它,就要决定让它成为引用对象还是不可变的值对象。
Inline Class[将类内联化]
场景
- 某个类没有做太多事情。
将这个类的所有特性搬移到另一个类中,然后移除原类。
动机
- Inline Class 与 Extract Class 相反。
- 如果一个类不再承担足够责任,不再有单独存在的理由,我就会挑选一个“萎缩类”的最频繁用户,以Inline Class 手法将“萎缩类”塞进另一个类中。
做法
- 在目标类身上声明源类的public协议,并将其中所有函数委托至源类。
- 修改所有源类引用点,改而引用目标类。
- 编译,测试。
- 运用Move Method和Move Field,将源类的特性全部搬移到目标类。
- 为源类举行一个简单的“丧礼”。
Hide Delegate[隐藏“委托关系”]
场景
- 客户通过一个委托类来调用另一个对象。
在服务类建立客户所需的所有函数,用以隐藏委托关系。
动机
- “封装”意味每个对象都应该尽可能少了解系统的其他部分。
- 如此一来,一旦发生变化,需要了解这一变化的对象就会比较少--这会使变化比较容易进行。
做法
- 对于每个委托关系中的函数,在服务对象端建立一个简单的委托函数。
- 调整客户,令它只调用服务对象提供的函数。
- 每次调整后,编译测试。
- 如果将来不再有任何客户需要用Delegate委托类,便可移除服务对象中的相关访问函数。
Remove Middle Man[移除中间人]
场景
- 某个类做了过多的简单委托动作。
让客户直接调用受托类
做法
- 建立一个函数,用以获得受托对象。
- 对于每个委托函数,在服类中删除该函数,并让需要调用该函数的客户转为调用受托对象。
- 处理每个委托函数后,编译,测试。
class Person ...
Department _department;
public Person getManger(){
return _department.getManger();
}
class Department ...
private Person _manager;
public Department (Person manager){
_manager = manager;
}
// 客户端调用
manager = john.getManager();
如果有大量函数都这么做,那么不得不在Person之中安置大量委托行为。
- 变成
class Person ...
Department _department;
public Person getManger(){
return _department;
}
class Department ...
private Person _manager;
public Department (Person manager){
_manager = manager;
}
// 客户端调用
manager = john.getManager().getManager();
Introduce Foreign Method[引入外加函数]
场景
- 你需要为提供服务的类增加一个函数,但你无法修改这个类。
在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。
做法
- 在客户类中建立一个函数,用来提供你需要的功能。
- 以服务类实例作为该函数的第一个参数。
- 将该函数注释为“外加函数,应在服务类中实现”。
Introduce Local Extension[引入本地拓展]
场景
- 你需要为服务类提供一些额外函数,但你无法修改这个类。
建立一个新类,使它包含这些额外函数。让这个扩展品称为源类的子类或包装类。
做法
- 建立一个扩展类,将它作为原始类的子类或包装类。
- 在扩展类中加入转型构造函数。
- 在扩展类中加入新特性。
- 根据需要,将原对象替换为扩展对象。将针对原始类定义的所有外加函数搬移到扩展类中。
第八章 重新组织数据
魔法数
- 也就是带有特殊含义的数字。
Self Encapsulate Field[自封装字段]
场景
- 你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙。
为这个字段建立取值/设值函数,并且只以这些函数来访问字段。
private int _low, _high;
boolean includes (int arg) {
return arg >= _low && arg <= _high;
}
- 变成
private int _low, high;
boolean includes (int arg) {
return arg >= getLow() && arg <= getHigh();
}
int getLow () {return _low;}
int getHigh () {return _high;};
动机
- “字段访问方式”这个问题上,两种截然不同的观点:
-
- 在该变量定义所在的类中,你可以自由访问它。
-
- 即使在这个类中你也应该只使用访问函数间接访问。
- 间接访问变量的好处:
-
- 子类可以通过腹泻一个函数而改变获取数据的途径。
-
- 它还支持更灵活的数据管理方式,例如延迟初始化(意思是:只有在需要用到某值时,才对它初始化)。
-
- 直接访问变量的好处:
-
- 代码比较容易阅读。
做法
- 为待封装字段建立取值/设值函数。
- 找出该字段的所有引用点,将他们全部改为调用取值/设值函数。
- 将该字段声明为private。
- 复查,确保找出所有引用点。
- 编译,测试。
Replace Data Value with Object[以对象取代数据值]
场景
- 你有一个数据项,需要与其他数据和行为一起使用才有意义。
将数据项变成对象。
做法
- 为待替换数值新建一个类,在其中生命一个final字段,其类型和源类中的待替换数值类型一样。然后在新类中加入这个字段的取值函数,再加上一个接受此字段为参数的构造函数。
- 编译。
- 将源类中的待替换数值字段的类型改为前面新建的类。
- 修改源类中该字段的取值函数,令它调用新类的取值函数。
- 如果源类构造函数中用到这个待替换字段(多半是赋值动作),我们就修改构造函数,令它改用新类的构造函数来对字段进行赋值动作。
- 修改源类中待替换字段的设置函数,令它为新类创建一个实例。
- 编译,测试。
- 现在,你有能耐需要对新类使用Change Value to Reference。
Change Value to Reference[将值对象改为引用对象]
场景
- 你从一个雷衍生出许多彼此相等的实例,希望将它们替换为同一个对象。
做法
- 使用Replace Constructor with Factory Method。
- 编译,测试。
- 决定由什么对象负责提供访问新对象的途径。
- 决定这些引用对象应该预先创建好,或是应该动态创建。
- 修改工厂函数,令它返回引用对象。
- 编译,测试。
Change Reference to Value[将引用对象改为值对象]
场景
- 你有一个引用对象,很小且不可变,而且不易管理。
做法
- 检查重构目标是否为不可变对象,或是否可修改为不可变对象。
- 建立equals()和hashCode()。
- 编译,测试。
- 考虑是否可以删除工厂函数,并将构造函数声明为public。
Replace Array with Object[以对象取代数组]
场景
- 你有一个数组,其中的元素各自代表不同的东西。
以对象替换数组。对于数组中的每个元素,以一个字段来表示。
String[] row = new String[3];
row[0] = "Liverpool";
row[1] = "15";
- 变成
Performance row = new Performance();
row.setName("Liverpool");
row.setWins("15");
做法
- 新建一个类表示数组所拥有的信息,并在其中以一个public字段保存原先的数组。
- 修改数组的所有用户,让它们改用新类的实例。
- 编译,测试。
- 逐一为数组元素添加取值/设值函数。根据元素的用途,为这些访问函数命名。修改客户端代码,让它们通过访问函数取用数组内的元素。每次修改后,编译并测试。
- 当所有对数组的直接访问都转而调用访问函数后,将新类中保存该数组的字段声明为private。
- 编译。
- 对于数组内的每一个元素,在新类中创建一个类型相当的字段。修改该元素的访问函数,令它改为上述的新建字段。
- 每修改一个元素,编译并测试。
- 数组的所有元素都有了相应字段之后,删除该数组。
Duplicate Observed Data[复制“被监视数据”]
场景
- 你有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。
将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。
动机
- 一个分层良好的系统,应该将处理用户和处理业务逻辑的代码分开。之所以这样做,是:
-
- 你可能需要使用不同的用户界面来表现相同的业务逻辑,如果同时承担两种责任,用户界面会变得过分复杂。
-
- 与GUI隔离之后,领域对象的维护和演化都会更容易,你甚至可以让不同的开发者负责不同部分的开发。
做法
- 修改展示类,使其成为领域类的Observer。
- 针对GUI类的领域数据,使用Self Encapsulate Field。
- 编译,测试。
- 在事件处理函数中调用设值函数,直接更新GUI组件。
- 编译,测试。
- 在领域类中定义数据及其相关访问函数。
- 修改展示类的访问函数,将它们的操作对象改为领域对象。
- 修改Observer的update(),使其从相应的领域对象中将所需数据复制给GUI组件。
- 编译,测试。
Change Unidirectional Association to Bidirectional[将单向关联改为双向关联] TODO
场景
- 两个类都需要使用对方特性,但其间只有一条单向连接。
添加一个反向指针,并使修改函数能够同时更新两条连接。
做法
- 在被引用类中增加一个字段,用以保存反向指针。
- 决定由哪个类--引用端还是被引用端--控制关联关系。
- 在被控端建立一个辅助函数,其命名应该清楚指出它的有限用途。
- 如果既有的修改函数在控制端,让它负责更新反向指针。
- 如果既有的修改函数在被控端,就在控制端建立一个控制函数,并让既有的修改函数调用这个新建的控制函数。
Change Bidirectional Association to Unidirectional[将双向关联改为单向关联] TODO
场景
- 两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。
去除不必要的关联。
Replace Magic Number with Symbolic Constant[以字面常量取代魔法数]
场景
- 你有一个字面数值,带有特别的函数。
创建一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。
double testArea (double high) {
return 3.14 * high;
}
- 变成
double testArea (double high) {
return WITH_CONSTANT * high;
}
static final double WITH_CONSTANT;
Encapsulate Field[封装字段]
场景
- 你的类中存在一个public字段。
将它声明为private,并提供相应的访问函数。
public String _name;
- 变成
private String _name;
public String getName () {
return _name;
}
public function void setName (String arg) {
_name = arg;
}
Encapsulate Collection[封装集合]
场景
- 有个函数返回一个集合。
让这个函数返回集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。
做法
- 加入为集合添加/移除元素的函数。
- 将保存集合的字段初始化为一个空集合。
- 编译。
- 找出集合设置函数的所有调用者。你可以修改那个设值函数,让它使用上述新建立的“添加/移除元素”函数;也可以直接修改调用端,改让它们调用上述新建立的“添加/移除元素”函数。
- 编译,测试。
- 找出所有“通过取值函数获得集合并修改其内容”的函数。逐一修改这些函数,让它们改用添加/移除函数。每次修改后,编译,测试。
- 修改完上述所有“通过取值函数获得集合并修改集合内容”的函数后,修改取值函数自身,使它返回该集合的一个只读副本。
- 编译,测试。
- 找出取值函数的所有用户,从中找出应该存在于集合所属对象内的代码。运用Extract Method 和 Move Method 将这些代码移到宿主对象中。
- 修改现有取值函数的名字,然后添加一个新取值函数,使其返回一个枚举。找出就取值函数的所有被用点,将他们都改为新取值函数。
- 编译,测试。
String[] getSkills() {
return _skills;
}
void setSkills (String[] arg) {
_skills = arg;
}
String [] _skills;
- 变成
void setSkill(int index, String newSkill) {
_skills[index] = newSkill;
}
String[] getSkills() {
return _skills;
}
Replace Record with Data Class[以数据类取代记录]
场景
- 你需要面对传统变成环境中的记录结构。
为该记录创建一个“哑”数据对象。
Replace Type Code with Class[以类取代类型码]
场景
- 类之中有一个类值类型码,但它并不影响类的行为。
以一个新类替换该数值类型码。
Replace Type Code with Subclasses[以子类取代类型码]
场景
- 你有一个不可变的类型码,它会影响类的行为。
以子类取代这个类型码。
做法
- 使用Self Encapsulate Field 将类型码自我封装起来。
- 为类型码每一个数值建立一个相应的子类。在每个子类中腹泻类型码的取值函数,使其返回相应的类型码值。
- 每建立一个新的子类,编译,测试。
- 从超类中删除保存类型码的字段。将类型码访问函数声明为抽象函数。
- 编译,测试。
Replace Type Code with State/Strategy[以State/Strategy取代类型码]
场景
- 你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它。
以状态对象取代类型码。
Replace Subclass with Fields[以字段取代子类]
场景
- 你的各个子类的唯一差别只在“返回常量数据”的函数身上。
修改这些函数,使它们返回超类中的某个新增字段,然后销毁子类。
第九章 简化条件表达式
Decompose Conditional[分解条件表达式]
场景
- 你有一个复杂的条件(if-then-else)语句。
从if、then、else三个段落中分别提取出独立函数。
if (date.before (SUMMER_STATE) || date.after(SUMMER_END)) {
charge = quantity * _winterRate + _winterServiceChange;
}else {
charge = quantity * _summerRate;
}
- 变成
if (notSummer(date)) {
charge = winterCharge(quantity);
}else {
charge = summerCharge(quantity);
}
Consolidate Conditional Expression[合并条件表达式]
场景
- 你有一系列条件测试,都得到相同结果。
将这些测试合并为一个条件表达式,将这个条件表达式提炼成为一个独立函数。
范例:使用逻辑或
double disabilityAmount() {
if (_seniority < 2) return 0;
if (_monthsDisabled > 12) return 0;
if (_isPartTime) return 0;
}
- 变成
double disabilityAmount() {
if (isNotEligableForDisability()) return 0;
}
boolean isNotEligableForDisability() {
return ((_seniority < 2) || (_monthsDisabled > 12) || (_isPartTime));
}
范例:使用逻辑与
if (onVacation())
if (lengthOfService() > 10)
return 1;
return 0.5;
- 变成
if (onVacation() && lengthOfService > 10 ) return 1;
else return 0.5;
- 使用三元操作变成
return (onVacation() && lengthOfService > 10 ) ? 1 : 0.5;
Consolidate Duplicate Conditional Fragments[合并重复的条件片段]
场景
- 在条件表达式的每个分支上有着相同的一段代码。
将这段重复代码移到条件表达式之外。
if (isSpecialDeal()) {
total = price * 0.95;
send();
}
else {
total = price * 0.95;
send();
}
- 变成
if (isSpecialDeal())
total = price * 0.95;
else
total = price * 0.98;
send();
Remove Control Flag[移除控制标记]
场景
- 在一系列布尔表达式中,某个变量带有“控制标记”的作用。
以break语句或return语句取代控制标记。
Replace Nested Conditional with Guard Clauses[以卫语句取代嵌套条件表达式]
场景
- 函数中的条件逻辑使人难以看清正常执行路径。
使用卫语句表现所有特殊情况
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[以多态取代条件表达式]
场景
- 你手上有个表达式,它根据对象类型的不同而选择不同的行为。
将这个条件表达式的每个分之放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。
Introduce Null Object[引入Null对象]
场景
- 你需要再三检查某对象是否为null。
将null值替换为null对象
if (customer == null) plan = BillingPlan.basic();
else plan = customer.getPlan();
第十章 简化函数调用
Rename Method[函数改名]
场景
- 函数的名称未能揭示其函数的用途。
修改函数名称。
getinvcdtlmt() {}
- 变成
getInvoiceableCreditLimit() {}
动机
- 给函数命名有一个好办法:首先考虑应该给这个函数写上一句怎样的注释,然后想办法将注释变成名称。
Add Prameter[添加参数]
场景
- 某个函数需要从调用端得到更多信息。
为此函数添加一个对象参数,让该对象待近函数所需信息。
Remove Parameter[移除参数]
场景
- 函数本体不再需要某个参数。
将该参数移除。
Separate Query from Modifier[将查询函数和修改函数分离]
场景
- 某个函数既返回对象状态值,又修改对象状态。
建立两个不同的函数,其中一个负责查询,另一个负责修改。
Parameterize Method[令函数携带参数]
场景
- 若干函数做了类似的工作,但在函数本体中却包含了不同值。
建立单一函数,以参数表达那些不同值。
void tenPercentRaise() {
salary *= 1.1;
}
void fivePercentRaise() {
salary *= 1.05;
}
- 变成
void raise(double factor) {
salary *= (1+factor);
}
Replace Parameter with Explicit Methods[以明确函数取代参数]
场景
- 你有一个函数,其中完全取决于参数值的不同行为。
针对该参数的每一个可能值,建立一个独立函数。
void setValue(String name, int value) {
if (name.equals("height")) {
_height = value;
return;
}
if (name.equals("width")) {
_width = value;
return;
}
Assert.shouldNerverReachHere();
}
- 变成
void setHeight(int arg) {
_height = arg;
}
void setWidth(int arg) {
_width = arg;
}
Preserve Whole Object[保持对象完整]
场景
- 你从某个对象中取出若干值,将他们作为某一此函数调用时的参数。
改为传递整个对象。
int low= daysTempRange().getLow();
int hiht = daysTempRange().getHigh();
withinPlan = plan.withinRange(low, high);
- 变成
withinPlan = plan.withinRange(daysTempRange());
Replaace Parameter with Methods[以函数取代参数]
场景
- 对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。
让参数接受者去除该项参数,并直接调用前一个函数。
int basePrice = _quantity * _itemPrice;
discountLevel = getDiscountLevel();
double finalPrice = discountedPrice (basePrice, discountLevel);
- 变成[如果函数可以通过其他途径获得参数值,name它就不应该通过参数取得该值。]
int basePrice = _quantity * _itemPrice;
double finalPrice = discountedPrice (basePrice, discountLevel);
Introduce Parameter Object[引入参数对象]
场景
- 某些参数总是很自然地同时出现。
以一个对象取代这些参数。
amountInvoicedln(start: Date, end:Date);
amountReceiveln(start: Date, end:Date);
amountOverdyeln(start: Date, end:Date);
- 变成
amountInvoicedln(DateRange);
amountReceiveln(DateRange);
amountOverdyeln(DateRange);
Remove Setting Method[移除设置函数]
场景
- 类中的某个字段应该在对象创建时被设值,然后就不再改变。
去掉该字段的所有设值函数。
class Account {
private String _id;
Account(String id) {
setId(id);
}
void setId(String arg) {
_id = arg;
}
}
- 变成
class Account {
private final String _id;
Account(String id) {
_id = $id;
}
}
Hide Method[隐藏函数]
场景
- 有一个函数,从来没有被其他任何类用到。
将这个函数修改为private。
Replace Constructor with Factory Method[以工厂函数取代构造函数]
场景
- 你希望在创建对象时不仅仅是做简单的构造动作。
将构造函数替换为工厂函数。
Employee(int type) {
_type = type;
}
- 变成
static Employee create(int type) {
return new Employee(type)
;}
Replace Error Code with Exception[以异常取代错误码]
场景
- 某个函数返回一个特定的代码,用以表示某种错误情况。
改用异常处理。
int withdraw(int amount) {
if (amount > _balance)
return -1;
else {
_balance -= amount;
return 0;
}
}
- 变成
void withdraw(int amount) throws BalanceException {
if (amount > _balance) throw new BalanceException();
_balance -= amount;
}
第十一章 处理概括关系
Pull Up Field[字段上移]
场景
- 两个子类拥有相同的字段。
将该字段移至超类。
abstract class Employee {
}
class SalesMan extends Employee {
public $name;
}
class Engineer extends Employee {
public $name;
}
- 变成
abstract class Employee {
public $name;
}
class SalesMan extends Employee {
}
class Engineer extends Employee {
}
Pull Up Method[函数上移]
场景
- 有些函数,在各个子类中产生完全相同的结果。
将该函数移至超类。
abstract class Employee {
}
class SalesMan extends Employee {
public function getName() {
}
}
class Engineer extends Employee {
public function getName() {
}
}
- 变成
abstract class Employee {
public function getName() {
}
}
class SalesMan extends Employee {
}
class Engineer extends Employee {
}
Pull Up Constructor Body[构造函数本体上移]
场景
- 你在各个子类中拥有一些构造函数,它们的本体几乎完全一致。
在超类中新键一个构造函数,并在子类构造函数中调用它。
Pull Down Method[函数下移]
场景
- 超类中的某个函数只与部分(而非全部)子类有关。
将这个函数移到相关的那些子类中。
abstract class Employee {
public function getQuota() {
}
}
class SalesMan extends Employee {
}
class Engineer extends Employee {
}
- 变成
abstract class Employee {
}
class SalesMan extends Employee {
public function getQuota() {
}
}
class Engineer extends Employee {
}
Push Down Field[字段下移]
场景
- 超类中的某个字段只被部分(而非全部)子类用到。
将这个字段移到需要它的子类中。
Extract Subclass[提炼子类]
场景
- 类中的某些特性只被某些(而非全部)实例用到。
新建一个子类,将上面所说的那一部分特性移到子类中。
Extract Superclass[提炼超类]
场景
- 两个类有相似的特性。
为这两个雷建立一个超类,将相同的特性移至超类。
Extract Interface[提炼接口]
场景
- 若干客户使用类接口中同一个子集,或两个类的接口有部分相同。
将相同的子集提炼到一个独立接口中。
Collapse Hierarchy[折叠继承体系]
场景
- 超类和子类之间无太大区别。
####### 将他们合为一体
塑造模板函数
场景
- 你有一些子类,其中响应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同。
将这些操作分别放进独立函数中, 并保持他们都有相同的签名,于是原函数也就变得相同了。然后将源函数上移至超类。
Replace Inheritance with Delegation[以委托取代继承]
场景
- 某个子类至使用超类接口中的一部分,或是根本不需要继承而来的数据。
在子类中新建一个字段用以保存超类;调整子类函数,令它改而委托超类;然后去掉两者之间的继承体系。
clas MyStack extends Vector {
public void push(Object element) {
。。。。
}
public Object pop() {
Object result = firstElement();
removeElementAt(0);
return result;
}
}
- 变成
class MyStack {
private Vector _vector = new Vector();
public boolean isEmpty() {
return _vector.isEmpty();
}
}
Replace Delegation with Inheritance[以继承取代委托]
场景
- 你在两个类之间使用委托关系,并经常为整个接口编写许多极为简单的委托函数。
让委托类继承受托类。
第十二章 大型重构
Tease Apart Inheritance[梳理并分解继承体系]
场景
- 某个继承体系同时承担两项责任。
建立两个继承体系,并通过委托关系让其中一个可以调用另一个。
动机
- 要指出继承体系是否承担了两项不同的责任并不困难:如果继承体系中的某以特定层级上的所有类,其子类名称都以相同的形容词开始,那么这个体系很可能是承担着两项不同的责任。
Convert Procedural Design to Objects[将过程化设计转化为对象设计]
场景
- 你手上有一些传统过程化风格的代码。
将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象之中。
Separate Domain from Presentation[将领域和表达/显示分离]
场景
- 某些GUI类之中包含了领域逻辑。
将领域逻辑分离出来,为它们建立独立的领域类。
动机
- MVC模式最核心的价值在于:它将用户界面代码(即视图;亦即现金常说的“展现层”)和领域逻辑(即模型)分离了。展现类只含用以处理用户界面的逻辑;领域类不含任何与程序外观相关的代码,只含业务逻辑相关代码。
Extract Hierarchy[提炼继承体系]
场景
- 你有某个类做了太多工作,其中一部分工作是一大量条件表达式完成的。