1 简介
知道“如何”不代表知道”何时”。决定何时重构、何时停止和知道重构机制如何运转一样重要。
从我们的经验来看,没有任何量度规矩比得上一个见识广博者的直觉。我们智慧告诉你一些迹象,它会指出“这里有一个可以用重构解决的问题”。你必须培养出自己的判断力,学会判断一个类内有多少实例变量算是太大,一个函数内有多少代码才算太长。
2 坏味道列表
2.1 重复代码Duplicated Code
如果你在一个以上的地点看到相同的程序结构,那么可以肯定:设法将它们合而为一,程序会变得更好。
2.1.1 Form Template Method塑造模板函数
你有一些子类,其中相应的某些函数以相同的顺序执行类似的操作,但各个操作的细节上有所不同。
将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了。然后将 原函数上移至超类。
继承是避免重复行为的一个强大工具。无论何时,只要你看见两个子类之中有类似的函数,就可以把它们提升到超类。但是如果这些函数并不完全相同该怎么办?我们仍有必要尽量避免重复,但又必须保持这些函数之间的实质差异。
常见的一种情况是:两个函数以相同的顺序执行大致相近的操作,但是各操作不完全相同。这种情况下,我们可以将执行操作的序列移至超类,并借助多态保证各个操作仍得以保持差异性。这样的函数被称为Template Method。
对于案例1最后的结果使用模板方法重构的结果如下:
2.2 过长函数Long Method
拥有短函数的对象会活得比较好、比较长。间接层所能带来的全部利益—解释能力、共享能力、选择能力----都是由小型函数所支持的。
注意:程序越长,越难理解
让小函数容易理解的真正关键在于一个好名字。应该积极的分解函数,遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途命名。
条件表达式和循环常常也是提炼的信号。可以使用Decompose Conditional来处理条件表达式,至于循环,应该将循环和其内的代码提炼到一个独立函数中。
2.3 过大的类Large Class
如果想利用单个类做太多事情,其内往往就会出现太多实例变量。这样就会出现太多重复代码。
可以使用Extract Class把几个变量一起提炼至新类内。提炼时应该选择类内彼此相关的变量,将它们放在一起。
2.4 过长参数列Long Parameter List
过去,老师告诉我们:把函数所需要的东西都以参数传递进去。而对象技术改变了这一点:如果你手上没有所需要的东西,总可叫另一个对象给你,有了对象,就不必把函数所需要的所有东西都以参数传递给它了,只需传给它足够的、让函数能够从中获得自己需要的东西就行。函数所需要的东西多半可以在函数的宿主类中找到。
太长的参数列难以理解,太多参数会造成前后不一致,不易使用,而且一旦你需要更多数据,就不得不修改它。可以把对象传递给函数,大多数修改都没有必要,因为你可能只需要在函数内增加一两条请求,就能得到更多数据。
如果向已有的对象发出一条请求就可以取代一个参数,则可以使用如下的重构手法:
2.4.1 Replace Parameter with Method用方法替换参数
对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数,让参数接受者去除该项参数,并直接调用前一个函数。
如果函数可以通过其他途径获得参数值,那么它就不应该通过参数取得该值。过长的参数列会增加程序阅读者的理解难度,因此我们应该尽可能的缩短参数列的长度。
2.4.2 Preserve Whole Object保持对象完整
可以使用Preserve Whole Object来将来自同一对象的一堆数据收集起来
你从某个对象中取出若干值,将它们作为某一次函数调用时的参数。改为传递整个对象。
通过这种方式可以提高代码的可读性。过长的参数列很难使用,因为调用者和被调用者都必须记住这些参数的用途。
2.4.3 Introduce Parameter Object
某些参数总是很自然的同时出现。
可以以一个对象取代这些参数。
你会看到特定的一组参数总是一起被传递。可能有几个函数都使用这一组参数,这些函数可能隶属于同一个类,也可能隶属于不同的类。这样一组参数就是所谓的数据泥团(Data Clumps),我们可以运用一个对象包装所有这些数据,再以一个对象封装所有这些数据,再以对象取代它们呢。本项重构的价值在于缩短参数列。
当你把这些参数组织到一起之后,往往很快可以发现一些可以被移至新建类的行为。通常,原本使用那些参数的函数对这一组参数会有一些共通的处理,如果将这些共通行为移到新对象中,就可以减少很多重复代码。
2.5 发散式变化Divergent Change
我们希望软件能够容易修改----一旦需要修改,我们希望能够跳到系统的某一点,只在此处做修改。如果不能做到这一点,你就嗅出两种紧密相关的刺鼻味道重的一种了。
如果某个类经常因为不同的原因在不同的方向上发生变化,Divergent Change就出现了。针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该反映此变化。
为此,应该找出特定原因而造成的所有变化,然后运用Extract Class了。
2.6 霰弹式修改Shotgun Surgery
如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是Shotgun Surgery。如果要修改的代码散落各处,你不但很难找到它们,也很容易遗忘某个重要的修改。
Divergent Change 是指一个类受多种变化的影响,而Shotgun Surgery则是指一种变化引发多个类做出相应修改。这两种情况下你都会希望整理代码,使外界变化与需要修改的类趋于一一对应。
2.6.1 Move Method
你的程序中,有个函数与其所驻类之外的另一个类进行更多交流,调用后者,或者被后者。
在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。
搬移函数是重构理论的支柱。如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,我就回搬移函数。
我常常会浏览类的所有函数,从中寻找这样的函数:使用另一个对象的次数比使用自己所驻对象的次数还多。一旦我移动了一些字段,就该做这样的检查。
2.6.2 Move Field
你的程序中,某个字段被其所驻类之外的另一个类有更多的用到。
在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段。
在类之间移动状态和行为,是重构过程中必不可少的措施。如果我发现,对于一个字段,在其所驻类之外的另一个类中有更多函数使用了它,我就会考虑搬移这个字段。
2.6.3 Inline Class
某个类没有做太多事情。
将这个类的所有特性搬移到另一个类中,然后移除原类。
Inline Class与Extract Class相反。如果一个类不再承担足够责任,不再有单独存在的理由,我就会挑选这一“萎缩类”的最频繁用户,以Inline Class手法将”萎缩类“塞进另一个类。
2.7 依恋情结Feature Enty
对象技术的全部要点在于:这是一种”将数据和对数据的操作行为包装在一起“的技术。有一种经典气味是:函数对某个类的兴趣高过对自己所处类的兴趣。这种孺慕之情最通常的焦点便是数据。因此可以使用Move Method把它移到它该去的地方。有时候函数中只有一部分受过这种状态之苦,这时候就应该使用Extract Method把这一部分提炼到函数中,再使用Method带它去它的梦中家园。
一个函数往往会用到几个类的功能,那么它应该被置于何处呢?我们的原则是:判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起。最根本的原则是:将总是一起变化的东西放在一块儿。数据和引用这些数据的行为总是一起变化的。
2.7.1 Extract Method
你有一段代码可以被组织在一起并独立出来。
将这段代码放进一个函数中,并让函数名称解释该函数的用途
Extract Method是Martin Fowler最喜欢的重构手法之一。当我看见一个过长的函数或者一段需要注释才能让人理解用途的代码,我就会将这段代码提炼到一个独立函数中。
具体做法如下:
2.8 数据泥团Data Clumps
数据项就像小孩子,喜欢成群结对地呆在一块儿。你常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起的数据真应该拥有属于它们自己的对象。可以使用Extract Class将它们提炼到一个独立对象中。然后将注意力转移到函数签名上,运用Introduce Parameter Object或Preserve Whole Object为它减肥,这样就可以将很多参数列缩短,简化函数调用。
2.8.1 Extract Class
某个类做了应该由两个类做的事。
建立一个新类,将相关的字段和函数从旧类搬移到新类。
一个类应该是一个清楚的抽象,处理一些明确的责任。
2.8.2 Introduce Parameter Object
参见2.4.3
2.8.3 Preserve Whole Object
参见2.4.2
2.9 基本类型偏执Primitive Obsession
大多数编程环境都有两种数据:结构类型允许你将数据组织成有意义的形式:基本类型则是构成结构类型的积木块。结构总是会带来一定的开销。
对象的一个极大的价值在于:它们模糊(甚至打破)了横亘于基本数据和体积较大的类之间的界限。
对象技术的新手通常不愿意在小任务上运用小对象----像是结合数值和币种的money类,由一个起始值和一个结束值组成的range类,电话号码或者邮政编码等等的特殊字符串。可以运用Replace Data Value with Object将原本单独存在的数据值替换为对象。如果你发现自己正在从数组中挑选数据,可运用Replace Array with Object。如果替换的数据值是类型码,而它并不影响行为,可以使用Replace Type code with Class。
2.9.1 Replace Data Value with Object
你有一个数据项,需要与其他数据和行为一起使用才有意义
将数据项变成对象
开发初期,你往往决定以简单的数据项表示简单的情况。但是,随着开发的进行,你可能会发现,这些简单的数据项不再那么简单了。比如说一开始你可能会用一个字符串来表示“电话号码”概念,但是随后就会发现,”格式化”,“抽取区号”之类的特殊行为。如果这样的数据项只有一两个,你还可以把相关的函数放进数据项所属的对象里:但是Duplicated Code坏味道和Feature Envy坏味道很快就会从代码中散发出来。但这些坏味道散发出来,你就应该将数据值变成对象。
2.9.2 Replace Array with Object
你有一个数组,其中的元素各自代表不同的东西。
以对象替换数组。对于数组中的每个元素,以一个字段来表示。
数组是一种常见的用以组织数据结构的结构。不过,它们应该只用于“以某种顺序容纳一组类似对象”。有时你会发现,一个数组容纳了多种不同的对象,这会给用户带来麻烦,因为他们很难记住像“数组中第一个元素是人名”这样的约定。对象就不同了,你可以运用字段名称和函数名称来传达这样的信息,因此你无需记住 它,也无需依赖注释。而且如果使用对象,还可以把信息封装起来,并用Move Method为它加上相关行为。
2.9.3 Replace Type code with Class
类之中有一个数值类型码,但它并不影响类的行为。
以一个新的类替换该数值类型码。
在以C为基础的编程语言中,类型码或枚举值很常见。如果带着一个youyiyi 的符号名,类型码的可读性还是不错的。但符号名终究只是个别名,编译期看见的,进行类型检查的还是背后那个数值。任何接收类型码作为参数的函数,所期望的实际上是一个数值,无法强制使用符号名。
如果把那样的数值转换成一个类,编译期就可以对这个类进行类型检验。只要为这个类提供工厂函数,你就可以始终保证只有合法的实例才会被创建过来,而且它们都会被传递给正确的宿主对象。更重要的是:任何使用switch语句的都应该运用Replace Conditional with Polymorphism去掉。
2.10 Switch惊悚现身Switch Statements
面向对象程序的一个最明显特征就是:少用switch或case语句。从本质上说,switch语句的问题在于重复。你常会发现同样的switch语句散步于不同地方。如果要为它添加一个新的case子句,就必须找到所有switch语句并修改它们。面向对象中的多态概念可以为此带来优雅的解决办法。
switch语句常常根据类型码进行选择,你要的是“与该类型码”相关的函数或类。所以应该使用Extract Method将switch语句提炼到一个独立函数中,再以Move Method 将它搬移到需要多态性的那个类里。此时可以使用Replace Type Code with Subclasses或Replace Type Code with State/Strategy。一旦这样完成继承结构之后,就可以使用Replace Conditional with Polymorphism.
2.10.1 Replace Type Code with Subclasses
如果你有一个不可变的类型码,它会影响类的行为
以子类取代这个类型码
如果你面对类型码不会影响宿主的行为,可以使用Replace code with Class来处理它们。但如果类型码会影响宿主类的行为,那么最好的办法就是借助多态来处理它们。
一般来说,这种情况的标志就是像switch这样的条件表达式。这种条件表达式可能有两种表现形式:switch语句或者if-then-else结构。不论哪种形式,它们都是检查类型码,并根据不同的值来执行不同的动作。首先应该将类型码替换为拥有多态行为的继承体系。这样一个继承体系应该以类型码的宿主类为基类,并针对每一种类型码各建立一个子类。
2.10.2 Replace Type Code with State/Strategy
你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它
以多态对象取代类型码
如果类型码的值在对象生命期内发生变化,或其他原因使得宿主类不能被继承。如果你打算搬移与状态相关的数据,而且你把新建对象视为一种变迁状态,就应该选择使用State模式。
2.10.3 Replace Conditional with Polymorphism
你手上有个条件表达式,它根据对象类型的不同而选择不同的行为。
将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数
在面向对象术语中,多态非常重要。多态最根本的好处就是:如果你需要根据对象的不同类型而采取不同的行为,多态使得你不必编写明显的条件表达式。
多态能够给我们带来很多好处,如果同一组条件表达式在程序许多地点出现,那么使用多态的收益是最大的。使用条件表达式时,如果你想添加一种新类型,就必须查找并更新所有条件表达式。如果改用多态,只需要建立一个新的子类,并在其中提供适当的函数就行了。类的用户不需要了解这个子类,这就大大降低了系统各部分之间的以来,使系统升级更加容易
2.11 平行继承体系Parallel InheritanceHierarchies
每当你为某个类增加一个子类,必须也为另一个类相应的增加一个紫劣。如果你发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同,便是闻到了这种坏味道。
消除这种重复性一般策略是:让一个继承体系的实例引用另一个继承体系的实力。
2.11.1 Tease Apart Inheritance梳理并分解继承体系
某个继承体系同时承担两项责任。
建立两个继承体系,并通过委托关系让其中一个可以调用另一个
2.12 冗余类Lazy Class
如果一个类不值得其身价,它就应该消失。因为你所创建的每一个类,都得有人去理解它、维护它,这些工作都是要花钱的。
2.13 夸夸其谈未来性Speculative Generality
“我想我们总有一天需要做这事“,并因而企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这种坏味道就出现了。
2.13.1 Collapse Hierarchy折叠继承体系
超类和子类无太大区别
将它们合为一体
2.13.2 Inline Class
参见2.6.3
使用Inline Class移除不必要的委托
2.13.3 Remove Paramter
如果函数的某些参数未被用上,可对它实施该重构手法。
函数本体不再需要某个参数。
将该参数去除
程序员可能经常添加参数,却 往往不愿意去掉它们。他们 打的如意算盘是:无论如何,多余的参数不会引起任何问题,而且以后还可能用上它。
这是恶魔的诱惑,一定要把它从脑子里赶出去!参数代表着函数所需的信息,不同的参数代表不同的意义。函数调用者必须为每一个参数操心该传什么东西进去。如果你不去掉多余参数,就是让你的每一位用户多操一份心。是很不划算的。
2.13.4 Rename Method
函数的名称未能揭示函数的用途
修改函数名称
我努力提倡的一种编程风格就是:将复杂的处理过程分解成小函数。关键就在于给函数起一个好名称。函数的名称应该准确表达它的用途。
注意:记住,你的代码首先是为人写的,其次是为计算机写的。
2.14 令人迷惑的暂时字段Temporary Field
有一种对象是这样的:其内某个实例变量仅为某种特定情况而设。
使用Extract Class为这个可怜的孤儿创造一个家,然后把所有和这个变量相关的代码都放进这个新家。也许可以使用Introduce Null Object在变量不合法的情况下创建一个Null对象,从而避免写出条件式代码。
2.14.1 Introduce Null Object
你需要再三检查某个对象是否为null。
将null值替换为Null对象
多态的根本好处在于:你不必再向对象询问你是什么类型而后根据得到的答案调用对象的某个行为----你只管调用行为就是了,其他的一切多态机制会为你安排妥当。
请记住:空对象一定是常量,它们的任何成分都不会发生变化,因此我们可以使用Singleton模式去实现它们。
2.15 过度耦合的消息链Message Chains
如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象。。。。这就是消息链。实际代码中,你可能看到的是一长串的getThis()或一长串临时变量。采取这种方法意味着客户代码将与查找过程中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端将不得不做出相应修改。
2.15.1 隐藏委托关系Hide Delegate
客户端通过一个委托类来调用另一个对象
在服务类上建立客户端所需的所有函数,用以隐藏委托关系
封装即使不是对象的最关键特征,也是最关键特征之一。封装意味着每个对象都应该尽可能少了解系统的其他部分。如此一来,一旦发生变化,需要了解这一变化的对象就会比较少----这会使变化比较容易进行。
如果客户希望知道某人的经理是谁,必须先获取Department对象:
manager = john.getDepartment().getManager();
这样的编码就是对客户端揭露了Department的工作原理,于是客户知道:Department用以追踪“经理”这些信息。如果对客户端隐藏Department,可以减少耦合,为了这个目的,在Person类中建立一个简单的委托函数:
public Person getManager() {
return _department.getManager();
}
这样,我得修改Person的所有客户,让它们改用新函数
manager = john.getManager();
只要完成了对Department所有函数的委托关系,并修改了Person的所有客户,我就可以移除Person中的访问函数getDepartment()了。
通常:更好的选择是:先观察消息链最终得到的对象是用来干什么的,看看是否能以Extract Method把使用该对象的代码提炼到一个独立函数中,再运用Move Method把这个函数推入消息链。如果这条消息链上的某个对象有多位客户端打算航行此航线的剩余部分,就加一个函数来做这件事。
2.16 中间人Middle Man
对象的基本特征之一就是封装----对外部世界隐藏其内部细节。封装往往伴随委托。比如你问主管是否有时间参加一个会议,他就把这个消息“委托”给他的记事簿,然后才能回答你。但是人们可能过度使用委托,你也许会看到某个类接口有一半的函数都委托给其他类,这样就是过度运用。这时可以使用Remove Middle Man,直接和真正负责的对象打交道。如果这样“不干实事”的函数只有少数几个,可以运用Inline Method把它们放进调用端。如果这些Middle Man还有其他行为,可以运用Replace Delegation with Inheritance把它变成实责对象的子类。
2.16.1 Remove Middle Man移除中间人
某个类做了过多的简单委托动作
让客户直接调用受托类
封装受托对象虽然有好处,但也有代价,代价就是:每当客户要使用受托类的新特性时,必须在服务端添加一个简单委托函数。随着受托类的特性功能越来越多,这一过程会让你痛苦不已。服务类完全变成了一个“中间人”,此时你就应该让用户直接调用受托类。
2.16.2 Inline Method内联函数
一个函数的本体与名称同样简单易懂
在函数调用点插入函数本体,然后移除该函数
注意:间接层有其价值,但不是所有的间接层都有价值
2.16.3 Replace Delegation with Inheritance
你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托关系
让委托类继承受托类
如果你发现自己需要使用受托类中的所有函数,并且费了很多力气编写所有极简单的委托函数,本重构可以帮助你轻松回头使用继承。
2.17 狎昵关系Inappropriate Intimacy
两个类过于密切,花费太多时间去探究彼此的private成分。这是不好的行为,我们希望类严守轻轨。过分狎昵的类必须拆散。Change Bidirectional Association to Unidirectional让其中一个类对另一个斩断情丝。
继承往往造成过度紧密,因为子类对超类的了解总是超过后者的主观愿望。如果你觉得该让这个孩子独自生活了,请运用Replace Inheritance with Delegation让它离开继承体系。
2.17.1 Replace Inheritance with Delegation
某个子类只使用超类接口的一部分,或是根本不需要继承而来的数据。
在子类中新建一个字段用以保存超类:调整子类函数,令它改而委托超类:然后去掉两者之间的继承关系。
或许,一开始你继承了一个类,随后发现超类中的许多操作并不真正shiyognyu 子类。这种情况下,你所拥有的接口并未真正反映出子类的功能。或者你发现你从超类继承了一大堆并不需要的数据。这样的结果是:代码传达的消息与你的意图南辕北辙----这是一种混淆,应该将其去除。
如果以委托取代继承,你可以更清楚的表明:你只需要委托类的一部分功能。接口中的那一部分应该被使用,那一部分应该被忽略,完全由你主导控制。这样做的成本则是需要额外写出委托函数,但这些函数都非常简单,极少可能出错。
2.17.2 Change Bidirectional Association to Unidirectional
两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性
去除不必要的关联
双向关联很有用,但是你也必须为它付出代价,那就是维护双向连接,确保对象被正确创建和删除而增加的复杂度。而且由于很多程序员并不习惯使用双向关联,它往往成为错误之源。
2.18 异曲同工的类Alternative Classes with Different Interfaces
如果两个函数做同一件事,却有着不同的签名,请运用Rename Method根据据他们的用途重新命名,但这往往不够,请反复运用Move Method将某些行为移入类,直到两者的协议一致为止。如果你必须重复而赘余德移入代码才能完成这些,或许可运用Extract Superclass为自己赎罪。
2.18.1 Rename Method重命名方法
参见2.13.4
2.18.2 Extract Superclass提炼超类
两个类具有相似特性
为这两个类建立一个超类,将相同特性移至超类。
重复代码是系统中最糟糕的东西之一。如果你在不同地方做同一件事,一旦需要修改那些代码,你就得平白做更多的修改。
重复代码的某种形式就是:两个类以相同的方式做类似的事情,或者以不同的方式做类似的事情。对象提供了一种简化这种情况的机制,那就是继承。另一种选择就是Extract Class,这两种方案之间的选择就是继承和委托之间的选择。
2.19 不完美的库类Incomplete Library Class
复用类被视为对象的终极目的。不过我们认为,复用的意义经常被高估----大多数对象只要够用就好。但库类构筑者没有未卜先知的能力,我们不能因此责怪他们。毕竟我们自己也几乎总是在系统快要构筑完成的时候才能弄清楚它的设计,所以库作者的任务真的很艰巨。麻烦的是往往库构造的不够好,而且往往不能修改其中的类使它完成我们希望的工作。可以使用如下两个工具来处理这种情况。如果只是想修改库类的一两个函数,可以运用Introduce Foreign Method,如果想要添加一大堆额外行为,就得运用Introduce Local Extension
2.19.1 Introduce Foreign Method引入外加函数
你需要为提供服务的类增加一个函数,但你无法修改这个类。
在客户类中建立一个函数,并以第一参数的形式传入一个服务类实例。
这种事情发生太多次了:你正在使用一个类,它真的很好,为你提供了需要的所有服务,而后,你又需要一个新服务,这个类却无法供应。于是你开始咒骂:“为什么不能做那件事”,如果可以修改源代码,你便可以自行添加一个函数;如果不能,你就得在客户端编码,补足你要的那个函数。
如果你需要多次使用类似的函数,就不得不重复这些代码,还记得吗,重复代码是万恶之源。这些重复代码应该被抽出放进一个函数中。进行本项重构时,如果你以外加函数实现一项功能,那就是一个明确函数,这个函数原本应该在提供服务的类中实现。
如果你发现自己为一个服务类建立了大量外加函数,或者发现有许多类都需要同样的外加函数,就不应该再使用本项重构,而应该使用Introduce Local Extension
在程序中,需要跨过一个收费周期。原本代码像这样:
Date newStart = new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate()+1);
可以将赋值运算右侧代码提炼到一个独立函数中。这个类就是Date类的一个外加函数。
Date newStrat = nextDay(previousEnd)
private static Date nextDay(Date arg) {
//foreign method, should bo on date
return new Date(arg.getYear(), arg.getMonth(), arg.getDate()+1);
}
注意:将类似的函数注释为:“外加函数foreign method”,应在服务类中实现,这么一来,如果将来有机会将外加函数搬移到服务类中时,你便可以轻松找出这些外加函数。
2.19.2 Introduce Local Extension引入本地扩展
你需要为服务类提供一些额外函数,但你无法修改这个类。
建立一个新类,使他包含这些额外函数,让这个扩展品成为源类的子类或包装类。
如果你可以修改源码,最好的办法就是直接加入自己需要的函数。但你经常需要修改源码。如果你只需要一两个函数,你可以使用Introduce Foreign Method。但如果你需要的额外函数超过两个,外加函数就很难控制它们了。所以,你需要将这些函数组织在一起,放到一个恰当的地方去。要达到这一目的,两种标准对象技术----子类化subclassing和包装wrapping是显而易见的办法。这种情况下,把子类或包装类统称为本地扩展。
2.20 纯稚的数据类Data Class
所谓Data Class是指:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。这样的类只是一种不会说话的数据容器,它们几乎一定被其他类过分细琐的操纵着。
Data Class 就像小孩子,作为一个起点很好,但若要让它们像成熟时的对象那样参与整个系统的工作,它们就必须承担一定的责任。
2.20.1 Encapsulate Field封装字段
你的类中存在一个public字段
将它声明为private,并提供相应的访问函数。
面向对象的首要原则之一就是封装,或者称为“数据封装”。按此原则你绝对不应该将数据声明为private,否则其他对象就有可能访问甚至修改修改这项数据,而拥有该数据的对象却毫无感觉。
通过这项重构手法,你可以将数据隐藏起来,并提供相应的访问函数。但它毕竟只是第一步。如果一个类除了访问函数不能提供其他行为,它终究只是一个哑巴类。这样的类不能享受对象技术带来的好处。而你知道,浪费任何一个对象都是很不好的。实施Encapsulate Field之后,我会尝试寻找到新建访问函数的代码,看看是否可以通过简单的Move Method轻快的将它们移动到新对象中。
2.20.2 Encapsulate Collection封装集合
有个函数返回一个集合
让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。
我们常常会在一个类中使用集合(collection, 可能是array, list、set或者Vector)来保存一组实例。这样的类通常也会提供针对该集合的取值/设值函数。
取值函数不该返回集合自身,因为这会让用户得以修改集合内容而集合拥有者拥有者却一无所晰。这也会对用户暴露过多对象内部数据结构的信息。如果一个取值函数确实需要返回多个值,它应该避免用户直接操作对象内所保存的集合,并隐藏对象内与用户无关的数据结构。
另外,不应该为这整个集合提供一个设值函数,但应该提供用以为集合添加/移除元素的函数,这样集合拥有者(对象)就可以控制集合元素的添加和移除。这样可以降低集合拥有者和用户之间的耦合度。
2.20.3 Remove Setting Method移除设值函数
类中的某个字段应该在对象创建时被设置,然后就不再改变
去掉该字段的所有设值函数
如果你为某个字段提供了设置函数,这就暗示这个字段值可以被改变。如果你不希望在对象被创建之后此字段还有机会被改变,那就不要为它提供设值函数,同时将该字段设置为final。这样你的意图会更加清晰,并且可以排除其值被修改的可能性----这种可能性往往是非常大的。
2.20.4 Hide Method隐藏函数
有一个函数,从来没有被其他任何类使用到
将这个函数设置为private
重构往往促使你修改函数的可见度。
2.21 被拒绝的遗赠Refused Bequest
子类应该继承超类的函数和数据,但如果它们不想或不需要继承,又该怎么办呢?它们得到所有礼物,却只从中挑选几样来玩。
你需要为这个子类新建一个兄弟类,再运用Push Down Method和Push Down Field把所有用不到的函数下推给那个兄弟。这样超类就只持有所有子类共享的东西。
如果子类复用了超类的行为(实现)却又不愿意支持超类的接口,Refused Bequest的坏味道就会变得浓烈。
2.21.1 Push Down Method
超类中某个函数只与部分(而非全部的)子类有关。
将这个函数移到相关的那些子类去。
2.21.2 Push Down Field
超类中的某个字段只被部分(而非全部)子类用到。
将这个子类移到需要它的那些子类去。
2.21.3 Replace Inheritance with Delegation
参见2.17.2
2.22 过多的注释Comments
从嗅觉上讲,Comments不是一种坏味道,事实上,它们还是一种香味呢。常常会看到这样的情况:你看到一段代码有着长长的注释,然后发现,这些注释之所以存在乃是因为代码很糟糕。这种情况的发生次数之多,实在令人吃惊。
如果你需要注释来解释一块代码做了什么,试试Extract Method;如果 函数已经提炼出来,但还是需要注释解释其行为,试试Rename Method;如果你需要注释说明某些系统的需求规格,试试Introduce Assertion。
注意:当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。
如果你不知道该做什么,这才是注释的良好运用时机。除了用来记述将来的打算之外,注释可以用来标记你并无十足把握的区域。你可以在注释里写下自己“为什么做某某事”,这类信息可以帮助将来的修改者,尤其是那些健忘的家伙。
2.22.1 Extract Method
参见2.7.1
具体做法如下:
2.22.2 Rename Method
参见2.13.4
注意:起一个好名称并不容易,需要经验:要想成为一个真正的编程高手,起名的水平是至关重要的。
2.22.3 Introduce Assertion引入断言
某一段代码需要对程序状态做出某种假设。
以断言明确表现这种假设。
常常会有这样一段代码:只有当某个条件为真时,该段代码才能正常运行。例如平方根计算只对正值才能进行。这样的假设通常并没有在代码中明确表现出来,你必须阅读整个算法才能看出。有时程序员会以注释写出这样的假设,但使用断言明确标明这些假设更好。
断言是一个条件表达式,应该总是为真,如果它失败,表明程序员犯了错误,因此断言的失败会导致一个非受控异常unchecked exception。断言绝对不能被系统的其他部分使用。实际上,程序最后的成品往往会将断言统统删除。
断言可以作为交流与调试的辅助。在交流的角度上,断言可以帮助程序阅读者理解代码所做的假设;在调试的角度上,断言可以在距离bug最近的地方抓住它们。
注意:断言的价值在于:帮助程序员理解代码正确运行的必要条件。
3 总结
《重构 改善既有代码的设计》这本书在看完之后,在总结这个代码的坏味道之时,也没想到会花费这么多的时间,今天是正月初七,2019年春节已经被留在了身后,自己也在家里呆了将近十天了。这个春节自己给父母一人买了一个红米note7,并不断的教他们使用,使用微信发送地址,图片,拍照,看视频,很开心。中间我爸在初四一次的喝酒喝到吐血,唉,我真的很担心我爸的身体,因为本身年纪已经64,并且我爸一点养生的意识都没有,还不断的怄气,操心,晚上吸烟很严重,希望以后我爸能少吸烟吧。
另外就是估计这次回去,自己会肥胖到新的高度吧,每在家里呆一天,就增加了19年自己减肥的难度,好尴尬。
本文主要是以代码的坏味道章节为蓝本,简要的把各个重构手法简要的罗列,以期能加深对重构的理解。
4 参考
《重构 改善既有代码的设计》之重构原则
《重构 改善既有代码的设计》之重构,第一个案例详解
《重构 改善既有代码的设计》之JUnit测试框架以及IDEA与JUnit整合
《重构 改善既有代码的设计》之代码的坏味道