重构-改善既有代码的设计读书笔记(三)
2016-09-26 07:58
309 查看
*表示此种情况较为抽象
使用Extract Method(参见6.1)提炼出重复代码,然后让两个地方调用这段代码。
两个互为兄弟的子类内包含相同表达式
对两个类都使用Extract Method(6.1),然后将被提炼出来的代码使用Pull Up Method(11.2),将它推入超类。
如果代码之间只是类似而并非完全相同,那么就得使用Extract Method(6.1)将相似部分和差异部分分割开,构成一个单独函数。然后可以尝试使用Form Template Method(11.10)获得一个Template Method设计模式。
如果有些函数以不同的算法做相同的事,你可以选择其中较为清晰的一个,使用Subsitute Algorithm(6.9)将其他函数的算法替换掉。
如果两个类毫不相干,可以考虑对其中一个使用Extract Class(7.3),将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类。
重复代码的函数也可能的确只应该属于某个类,另一个类只能调用它,抑或这个函数属于第三个类,而另两个类应该引用第三个类。
注意:你必须决定这个函数放在哪最合适,并确保它被安置后不会再在其他任何地方出现。
重构过长函数遵循的原则 :每当感觉需要用注释来说明点什么的时候,我们就需要把说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。
找到函数中适合集中在一起的部分,使用Extract Method(6.1)将他们提炼出来形成一个新函数
如果函数内有大量的参数和临时变量,这会对重构形成阻碍。直接使用Extract Method会把参数和临时变量当作参数传递给新函数,导致可读性几乎没有提升。此时,你可以经常运用Replace Temp with Query(6.4)来消除这些临时元素。Introduce Parameter Object(10.9)和Preserve Whole Object(10.7)则可以将过长的参数列变得更简洁一些。
如果上面的做法,还留有太多临时变量和参数,可以考虑使用Replace Method with Method Object(6.8)。
寻找注释
注释通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就可以将这段代码替换为一个函数。
条件表达式和循环常常也是提炼的信号
可以使用Decompose Conditional(9.1)处理条件表达式。至于循环,应该将循环和其内的代码提炼到一个独立函数中。
解决:针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该反应此变化。为此,你应该找出某特定原因而造成的所有变化,然后运用Extract Class(7.3)将他们提炼到另一个类中。
解决:应该使用Move Method(7.1)和Move Field(7.2)把所有需要修改的代码放进同一个类。如果没有合适的类,就创造一个。通常可以运用Inline Class(7.4)把一系列相关行为放进同一个类。但要注意,这可能会造成少量的Divergent Change。
以上两种变化的最终结果,使“外界变化”与“需要修改的类”趋于一 一对应。
如果你的某个抽象类其实没有太大作用,请运用Collapse Hierarchy(11.9)。
不必要的委托可以运用Inline Class(7.4)除掉。
如果函数的某些参数未被用上,可对他实施Remove Parameter(10.3)。
如果函数名称带有多余的抽象意味,应该对他实施Rename Method(10.1)。
可以运用Move Method(7.1)和Move Field(7.2)帮他们划清界限。
可以看看是否可以运用Change Bidirectional Association to Unidirectional(8.8)让一个类对另一个断绝关系。
如果两个类确实需要有联系,可以运用Extract Class(7.3)把两者共同点提炼到一个安全地点,让它们自由使用这个新类。
或者也可以使用Hide Delegation(7.5)让第三方类来联系他们。
继承往往造成两个类联系过于紧密,如果觉得子类可以单独抽出,使用Replace Inheritance with Delegation(11.11)让它离开继承体系。
如果只想修改类库的一两个函数,可以运用Introduce Foreign Method(7.7)。
如果想要添加一大堆额外行为,就得运用Introduce Local Extension(7.8)。
这些类早期可能拥有public字段,这样的话你应该使用Encapsulate Field(8.10)将他们封装起来。
如果这些类内含容器类的字段,你应该检查他们是不是得到了恰当的封装:如果没有,就运用Encapsulate Collection(8.11)把他们封装起来。
对于那些不该被其他类修改的字段,运用Remove Settion Method(10.10)。
然后找出这些取值/设置函数被其他类调用的地点。尝试以Move Method(7.1)把那些调用行为搬移到Data Class来。
如果无法搬移整个函数,就运用Extract Method(6.1)产生一个可被搬移的函数。之后你就可以运用Hide Method(10.11)把这些取值/设置函数隐藏。
子类不想或不需要继承超类的所有函数和数据。
子类复用了超类的行为,却又不愿意支持超类的接口。
解决:
你需要为前者的子类新建一个兄弟类,再运用Push Down Method()和Push Down Field()把所有用不到的函数下推给那个兄弟。
对于后者,拒绝继承超类的实现,这一点不需要介意;但如果拒绝继承超类的接口,就不行了。即使你不愿意继承接口,也不要胡乱修改继承体系,应该运用Replace Inheritance with Delegation(11.11)。
如果需要注释来解释一块代码做了什么,试试Extract Method(6.1)。
如果函数已经提炼出来,但还需要注释来解释其行为,试试Rename Method(10.1)。
如果你需要注释说明某些系统的需求规格,试试Introduce Assertion(9.8)。
代码的坏味道
3.1 Duplicated Code(重复代码)
同一个类的两个函数含有相同表达式使用Extract Method(参见6.1)提炼出重复代码,然后让两个地方调用这段代码。
两个互为兄弟的子类内包含相同表达式
对两个类都使用Extract Method(6.1),然后将被提炼出来的代码使用Pull Up Method(11.2),将它推入超类。
如果代码之间只是类似而并非完全相同,那么就得使用Extract Method(6.1)将相似部分和差异部分分割开,构成一个单独函数。然后可以尝试使用Form Template Method(11.10)获得一个Template Method设计模式。
如果有些函数以不同的算法做相同的事,你可以选择其中较为清晰的一个,使用Subsitute Algorithm(6.9)将其他函数的算法替换掉。
如果两个类毫不相干,可以考虑对其中一个使用Extract Class(7.3),将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类。
重复代码的函数也可能的确只应该属于某个类,另一个类只能调用它,抑或这个函数属于第三个类,而另两个类应该引用第三个类。
注意:你必须决定这个函数放在哪最合适,并确保它被安置后不会再在其他任何地方出现。
3.2 Long Method(过长函数)
注意给函数起一个好名字,这是小函数易于理解的关键。重构过长函数遵循的原则 :每当感觉需要用注释来说明点什么的时候,我们就需要把说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。
找到函数中适合集中在一起的部分,使用Extract Method(6.1)将他们提炼出来形成一个新函数
如果函数内有大量的参数和临时变量,这会对重构形成阻碍。直接使用Extract Method会把参数和临时变量当作参数传递给新函数,导致可读性几乎没有提升。此时,你可以经常运用Replace Temp with Query(6.4)来消除这些临时元素。Introduce Parameter Object(10.9)和Preserve Whole Object(10.7)则可以将过长的参数列变得更简洁一些。
如果上面的做法,还留有太多临时变量和参数,可以考虑使用Replace Method with Method Object(6.8)。
寻找注释
注释通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就可以将这段代码替换为一个函数。
条件表达式和循环常常也是提炼的信号
可以使用Decompose Conditional(9.1)处理条件表达式。至于循环,应该将循环和其内的代码提炼到一个独立函数中。
3.3 Large Class(过大的类)
可以运用Extract Class(7.3)将几个变量一起提炼至新类。提炼时应该选择类内彼此相关的变量。
3.4 Long Parameter List(过长参数列)
如果向已有的对象发出一条请求就可以取代一个参数,那么你应该激活重构手法Replace Parameter with Method(10.8)。在这里,“已有的对象”可能是函数所属类内的一个字段,也可能是另一个参数。 你还可以使用Preserve Whole Object(10.7)将来自同一对象的一堆数据收集起来,并以该对象替换他们。如果某些数据缺乏合理的对象归属,可使用Introduce Parameter Object(10.9)为他们制造出一个“参数对象”。
*3.5 Divergent Change(发散式变化)
现象:某个类经常因为不同的原因在不同的方向上发生变化。(一个类受多种变化的影响)解决:针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该反应此变化。为此,你应该找出某特定原因而造成的所有变化,然后运用Extract Class(7.3)将他们提炼到另一个类中。
*3.6 Shotgun Surgery(散弹式修改)
现象:和发散式变化相反。如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改。(一种变化引发多个类相应修改)解决:应该使用Move Method(7.1)和Move Field(7.2)把所有需要修改的代码放进同一个类。如果没有合适的类,就创造一个。通常可以运用Inline Class(7.4)把一系列相关行为放进同一个类。但要注意,这可能会造成少量的Divergent Change。
以上两种变化的最终结果,使“外界变化”与“需要修改的类”趋于一 一对应。
3.7 Feature Envy(依恋情结)
通常我们都将数据和对数据的操作行为包装在一起作为一个类。但如果函数对某个类的兴趣高过对自己所处的类的兴趣,Featuer Envy就出现了。 处理原则:判断哪个类拥有最多被此函数使用的数据,然后把这个函数和那些数据放在一起。如果先以Extract Method(6.1)将这个函数分解为几个较小函数并分别放置于不同地点,更容易实现上面的做法。
3.8 Data Clumps(数据泥团)
现象:在很多地方看到相同的数据,比如两个类中相同的字段、许多函数签名中相同的参数。 解决:首先请找出这些数据以字段形式出现的地方,运用Extract Class(7.3)将他们提炼到一个独立对象中。然后将注意力转移到函数签名上,运用Introduce Parameter Object(10.9)或Preserve Whole Object(10.7)简化函数签名。得到新对象后,就可以着手寻找Featuer Envy ,这可以帮你指出能够移至新类中的种种程序行为。
3.9 Primitive Obsession(基本类型偏执)
我们应该更愿意用一些小对象来替换基本型数据。如果要替换的数据值是类型码,而它并不影响行为,则可以运用Replace Type Code with Class(8.13)将它替换掉。 如果你有与类型码相关的条件表达式,可运用Replace Type Code with Subclss(8.14)或Replace Type Code with State/Strategy(8.15)加以处理。 如果有一组应该总是被放在一起的字段,可运用Extract Class(7.3)。 如果你在参数列中看到基本型数据,不妨试试Introduce Parameter Object(10.9)。
3.10 Switch Statements(switch惊悚献身)
解决方案: 多数情况下,一看到switch语句,你就应该考虑以多态来替换他。switch语句常常根据类型码进行选择,你要的是”与该类型码相关的函数或类“,所以应该使用Extract Method(6.1)将switch语句提炼到一个独立函数中,再以Move Method(7.1)将他搬移到需要多态性的那个类里。此时你必须决定是否使用Replace Type Code with Subclass(8.14)或Replace Type Code with State/Strategy(8.15)。一旦这样完成继承结构之后,你就可以运用Replace Conditional with Polymorphism(9.6)。 如果你只是在单一函数中有些选择事例,且并不想改动他们,可以考虑使用Replace Parameter with Explicit Methods(10.6)。 如果你的选择条件之一是null,可以试试Introduce Null Object(9.7)。
3.11 Parallel Inheritance Hierarchies(平行继承体系)
特征:每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。如果你发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同,即是这种情况。 解决:让一个继承体系的实例引用另一个继承体系的实例。之后可以使用Move Method(7.1)和Move Field(7.2),就可以将引用端的继承体系消除。
3.12 Lazy Class (冗赘类)
特征:如果一个类的所得不值其身价,他就应该消失。例如,某个类原本对得起自己的身价,但重构使它身形缩水,不再做那么多的工作;或开发者事前规划了某些变化,并添加一个类来应付这些变化,但变化实际上没有发生。 解决:如果某些子类没有做足够的工作,试试Collapse Hierarchy(11.9)。对于几乎没用的组件,你应该以Inline Class(7.4)对付他们。
3.13 Speculative Generality(夸夸其谈未来性)
特征:过多考虑未来的情况,增加了系统的复杂度。 解决:
如果你的某个抽象类其实没有太大作用,请运用Collapse Hierarchy(11.9)。
不必要的委托可以运用Inline Class(7.4)除掉。
如果函数的某些参数未被用上,可对他实施Remove Parameter(10.3)。
如果函数名称带有多余的抽象意味,应该对他实施Rename Method(10.1)。
3.14 Temporary Field(令人迷惑的暂时字段)
特征:对象内的某个实例变量仅为某种特定情况而设。这样的代码让人不易理解,因为你通常认为对象在所有时候都需要他的所有变量。 解决:使用Extract Class(7.3)创建一个新类,然后把所有和这个变量相关的代码都放进这个类。也许你还可以使用Introduce Null Object(9.7)在”变量不合法“的情况下创建一个null对象,从而避免写出条件式代码。 如果类中有一个复杂算法,需要好几个变量,往往就会导致Temporary Field。而传递的一长串参数只在使用这个算法时才有效,那么我们就需要使用Extract Class(7.3)把这些变量和其相关函数提炼到一个独立类中。
3.15 Message Chains(过度耦合的消息链)
特征:如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象......这就是消息链。实际代码中你看到的可能是一长串getThis()或一长串临时变量。采取这种方式,客户端代码将与查找过程中的导航结构紧密耦合。 解决: 使用Hide Delegate(7.5)。你可以在消息链的不同位置进行这种重构手法。理论上可以重构消息链的任何一个对象,但这么做往往会把一系列对象都变成Middle Man。通常更好的选择是:先观察消息链最终得到的对象是用来干什么的,看看能否以Extract Method(6.1)把使用该对象的代码提炼到一个独立函数中,再运用Move Method(7.1)把这个函数推入消息链。如果这条链上的某个对象有多位客户打算使用剩余的部分,就加一个函数来做这件事。
3.16 Middle Man(中间人)
特征: 过度运用委托。你可能会看到某个类接口有一半函数都委托给其他类,这就是过度运用。 解决:应该使用Remove Middle Man(7.6),直接和真正负责的对象打交道。如果这样”不干实事“的函数只有少数几个,可以运用Inline Method(6.2)把他们放进调用端。如果这些Middel Man还有其他行为,可以运用Replace Delegation with Inheritance(11.12)把他变成实责对象的子类,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。
3.17 Inappropriate Intimacy(狎昵关系)
特征: 两个类关系过于亲密。 解决:
可以运用Move Method(7.1)和Move Field(7.2)帮他们划清界限。
可以看看是否可以运用Change Bidirectional Association to Unidirectional(8.8)让一个类对另一个断绝关系。
如果两个类确实需要有联系,可以运用Extract Class(7.3)把两者共同点提炼到一个安全地点,让它们自由使用这个新类。
或者也可以使用Hide Delegation(7.5)让第三方类来联系他们。
继承往往造成两个类联系过于紧密,如果觉得子类可以单独抽出,使用Replace Inheritance with Delegation(11.11)让它离开继承体系。
3.18 Alternative Classes with Different Interfaces(异曲同工的类)
特征:两个函数做同一件事,却有着不同的签名。 解决:运用Rename Method(10.1)根据他们的用途重新命名。另外,还需要反复运用Move Method(7.1)将某些行为移入类,直到两者的协议一致为止。
3.19 Incomplete Library Class(不完美的类库)
特征:类库往往构造的不够好,而且往往不可能让我们修改其中的类使他完成我们希望完成的工作。 解决:
如果只想修改类库的一两个函数,可以运用Introduce Foreign Method(7.7)。
如果想要添加一大堆额外行为,就得运用Introduce Local Extension(7.8)。
3.20 Data Class(纯稚的数据类)
特征:只拥有一些字段和用于访问这些字段的函数的类。 解决:
这些类早期可能拥有public字段,这样的话你应该使用Encapsulate Field(8.10)将他们封装起来。
如果这些类内含容器类的字段,你应该检查他们是不是得到了恰当的封装:如果没有,就运用Encapsulate Collection(8.11)把他们封装起来。
对于那些不该被其他类修改的字段,运用Remove Settion Method(10.10)。
然后找出这些取值/设置函数被其他类调用的地点。尝试以Move Method(7.1)把那些调用行为搬移到Data Class来。
如果无法搬移整个函数,就运用Extract Method(6.1)产生一个可被搬移的函数。之后你就可以运用Hide Method(10.11)把这些取值/设置函数隐藏。
3.21 Refused Bequest(被拒绝的遗赠)
特征:子类不想或不需要继承超类的所有函数和数据。
子类复用了超类的行为,却又不愿意支持超类的接口。
解决:
你需要为前者的子类新建一个兄弟类,再运用Push Down Method()和Push Down Field()把所有用不到的函数下推给那个兄弟。
对于后者,拒绝继承超类的实现,这一点不需要介意;但如果拒绝继承超类的接口,就不行了。即使你不愿意继承接口,也不要胡乱修改继承体系,应该运用Replace Inheritance with Delegation(11.11)。
3.22 Comments(过多的注释)
特征:你看到一段代码有着长长的注释,然后发现,这些注释之所以存在是因为代码很糟糕。 解决:
如果需要注释来解释一块代码做了什么,试试Extract Method(6.1)。
如果函数已经提炼出来,但还需要注释来解释其行为,试试Rename Method(10.1)。
如果你需要注释说明某些系统的需求规格,试试Introduce Assertion(9.8)。
相关文章推荐
- 代码的坏味道之二十二 :Comments(过多的注释)
- 代码的坏味道之二十一 :Refused Bequest(被拒绝的遗贈)
- 代码的坏味道之二十 :Data Class(纯稚的数据类)
- 代码的坏味道之十八 :Alternative Classes with Different Interfaces(异曲同工的类)
- 代码的坏味道之十七 :Inappropriate Intimacy(狎昵关系)
- 代码的坏味道之十六 :Middle Man(中间转手人)
- 代码的坏味道之十五 :Message Chains(过度耦合的消息链)
- 代码的坏味道之十四 :Temporary Field(令人迷惑的暂时值域)
- 代码的坏味道之十三 :Speculative Generality(夸夸其谈未来性)
- 代码的坏味道之十二 :Lazy Class(冗赘类)
- 代码的坏味道之十一 :Parallel Inheritance Hierarchies(平行继承体系)
- 代码的坏味道之十 :Switch Statements(switch惊悚现身)
- 代码的坏味道之九 :Primitive Obsession(基本型别偏执)
- 代码的坏味道之八 :Data Clumps(数据泥团)
- 代码的坏味道之六 :Shotgun Surgery(散弹式修改)
- 代码的坏味道之五 :Divergent Change(发散式变化)
- 代码的坏味道之四 :Long Parameter List(过长参数列)
- 代码的坏味道之三 :Large Class(过大类)
- 代码的坏味道之二 :Long Method(过长函数)
- 代码的坏味道之一 :Duplicated Code(重复的代码)