重构-改善既有代码的设计:处理概括关系 (九)
1. Pull Up Field 字段上移
两个子类拥有相同的字段。将该字段移至超类。
如果各子类是分别开发的,或者是在重构过程中组合起来的,你常会发现它们拥有重复特性,特别是字段更容易重复。这样的字段有时拥有相似的名字,但也并非绝对如此。判断若干字段是否重复,唯一的办法就是观察函数如何使用它们。如果它们被使用的方式很相似,你就可以将它们归纳到超类去。
2. Pull up Method 函数上移
有些函数,在各个子类中产生完全相同的结果。将该函数移至超类。
避免重复行为是很重要的。尽管重复的2个函数也可以各自工作的很好,但重复自身只会成为错误的滋生地,此外别无价值。无论何时,只要系统内出现重复,你就面临“修改其中一个却未能修改另一个”的风险。通常,找出重复也有一定困难。
如果某个函数在各子类中的函数体相同,这就是做显而易见的Pull Up Method (方法上移)适用场合。当然,情况并不总是如此明显。你也可以只管放心重构,再看看测试程序会不会发牢骚,但这就需要对你的测试有充分的信心。观察这些可能重复的函数之间的差异往往大有收获:它们经常会展示那些忘记测试的行为。
Pull Up Method (方法上移)常常紧随其他重构而被使用。也许你能找出若干个身处不同子类的函数,而它们又可以通过某种形式的参数调整成为相同的函数。这时候,最简单的办法就是首先分别调整这些函数的参数,然后再将它们概况到超类中。当然,如果你足够自信,也可以一次完成这2个步骤。
有一种特殊情况也需要使用Pull Up Method (方法上移):子类的函数覆写了超类的函数,但仍然做相同的工作。
Pull Up Method (方法上移)过程中最麻烦的一点就是:被提升的函数可能会引用只出现于子类而不出现于超类的特性。如果被引用的是个函数,你可以将该函数也一同提升到超类,或者在超类中建立一个抽象函数。在此过程中,你可能需要修改某个函数的签名,或建立一个委托函数。
如果2个函数相似但不相同,你或许可以先借助Form Template Method (塑造模板函数)构造出相同的函数,然后再提升它们
3. Pull up Constructor Body 构造函数本体上移
你在各个子类中拥有一些构造函数,它们的本体几乎完全一致。在超类中新建一个构造函数,并在子类构造函数中调用它。
构造函数是很奇妙的东西。它们不是普通函数,使用它们比使用普通函数受到更多的限制。
如果你看见各个子类中的函数有共同的行为,第一个念头应该是将共同行为提炼到一个独立函数中,然后将这个函数提升到超类。对构造函数而言,它们彼此的共同行为往往就是“对象的构建”。这时候你需要在超类中提供一个构造函数,然后让子类都来调用它。很多时候,子类构造函数的唯一动作就是调用超类构造函数。这里不能运用 Pull Up Method (方法上移),因为你无法在子类中继承超类构造函数。
如果重构过程过于复杂,你可以考虑使用 Replace Constructor with Factory Method (以工厂函数取代构造函数)。
4. Push down Method 函数下移
超类中的某个函数只与部分子类有关。将这个函数移到相关的那些子类去。
push down Method (函数下移)和Pull Up Method (函数上移)恰恰相反,当有必要把某些行为从超类移至特定的子类时,就使用push down Method (函数下移),它通常也只在这种时候使用。使用Extract Subclass (提炼子类)之后可能会需要它.
5. Push down Fiedld 字段下移
超类中的某个字段只被部分子类用到,将这个字段移到需要它的那些子类去。
如果只有某些子类需要超类内的一个字段,那就可以使用本项重构。
6. Extract Subclass 提炼子类
类中的某些特性只被某些实例用到。新建一个子类,将上面所说的那一部分特性移到子类中。
使用Extract Subclass (提炼子类)的主要动机是:你发现类中的某些行为只被一部分实例用到,其他实例不需要它们。有时候这种行为上的差异是通过类型码区分的,此时你可以使用Replace Type Code with Subclass (以子类取代类型码)或Replace Type Code with State/Strategy (以状态策略取代类型码)。但是,并非一定要出现了类型码才表示需要考虑使用子类。
Extract Class (提炼类)是Extract Subclass (提炼子类)之外的另一个选择,2者之间的抉择其实就是委托和继承之间的抉择。Extract Subclass (提炼子类)通常更容易进行,但它也有限制:一旦对象创建完成你无法再改变与类型的相关行为,但如果使用Extract Class (提炼类),你只需插入另一个组件就可以改变对象的行为。此外,子类只能用以表现一组变化。如果你希望一个类以几种不同的方式变化,就必须使用委托。
7. Extract Superclass 提炼超类
两个类有相似特性。为这2个类建立一个超类,将相同特性移至超类。
重复代码是系统中最糟糕的东西之一。如果你在不同地方做同一件事情,一旦需要修改那些动作,你就得平白做更多的修改。
重复代码的某种形式就是:2个类以相同的方式做类似的事情,或者以不同的方式做类似的事情。对象提供了一种简化这种情况的机制,那就是继承。但是,在建立这些具有共通性的类之前,你往往无法发现这样的共通性,因此常常会在具有共通性的类出现之后,再开始建立其间的继承结构。
另一种选择就是 Extract Class(提炼类)。这2种方案之间的选择其实就是继承和委托之间的选择。如果2个类可以共享行为,也可以共享接口,那么继承是比较简单的做法。如果你选错了,也总有 Replace Inheritance with Delegation (以委托取代继承)这瓶后悔药可吃。
8. Extract Interface 提炼接口
若干客户使用类接口中的同一子集,或者2个类的接口部分相同。将相同的子集提炼到一个独立接口中。
类之间彼此互用的方式有若干种。“使用一个类”通常意味着用到该类的所有责任区。另一种情况是,某一组客户只使用类责任区中的一个特定子集。再一种情况是,这个类需要与所有协助处理某些特定请求的类合作。
对于后2种情况,将真正用到的这部分责任分离出来通常很有意义,因为这样可以使系统的用法更清晰,同时也更容易看清系统的责任划分。如果新的类需要支持上述子集,也比较能够看清子集内有些什么东西。
在许多面向对象语言中,这种责任划分是通过多继承来实现的。在c#中可以运用接口来诏示并实现上述需求。
Extract Subclass (提炼子类)和Extract Interface (提炼接口)之间有些相似之处。Extract Interface (提炼接口)只能提炼共通接口,不能提炼共通代码。使用Extract Interface (提炼接口)可能造成难闻的“重复”坏味道,幸而你可以运用Extract Class(提炼类)先把共通行为放进一个组件中,然后将工作委托给该组件,从而解决这个问题。如果有不少共通行为,Extract Superclass (提炼超类)会比较简单,但是每个类只能有一个超类。
如果某个类在不同环境下扮演截然不同的角色,使用接口就是个好主意。你可以针对每个角色以Extract Interface (提炼接口)提炼出相应接口。另一种可以用Extract Interface (提炼接口)的情况是:你想要描述一个类的外部依赖接口。如果你打算将来加入其它种类的服务对象。只需要求它们实现这个接口即可。
9. Collapse Hierarchy 折叠继承体系
超类和子类之间无太大区别。将它们和为一体。
如果你曾经编写过继承体系,就会知道,继承体系很容易变得过分复杂。所谓重构继承体系,往往是将函数和字段在体系中上下移到。完成这些动作后,你很可能发现某个子类并未带来该有的价值,因此需要把超类和子类合并起来。
10. From TemPlate Method 塑造模板函数
你有一些子类,其中相应的某些函数以相同的顺序执行类似的操作,但各个操作的细节不同。将这些操作分别放进独立的函数中,并保持它们都有相同的签名,于是原函数也就变得相同了,然后将原函数上移至超类。
继承是避免重复行为的一个强大工具。无论何时,只要你看见2个子类之中有类似的函数,就可以把它们提升到超类。但是如果这些函数并不完全相同该这么办?仍有必要尽量避免重复,但又必须保持这些函数之间的实质差异。
常见的一种情况是:2个函数以相同的顺序执行大致相近的操作,但是各操作不完全相同。这种情况下我们可以将执行的序列移至超类,并借助多态保证各操作仍得以保持差异性。这样的函数被称为Template Method(模板函数)。
11. Replace Inheritance with delegation 以委托取代继承
某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据。在子类中新建一个字段用以保存超类;调整子类函数,令它改而委托超类;然后去掉2者之间的继承关系。
继承是个好东西,但有时候它并不是你要的。你常常会遇到这样的情况:一开始继承了一个类,随后发现超类中的许多操作并不真正适用于子类。这种情况下,你所拥有的接口并未真正反映出子类的功能。或者,你可能发现你从超类中继承了一大堆子类并不需要的数据,抑或你可能发现超类中的某些protected函数对子类并没有什么意义。
你可以选择容忍,并接受传统说法:子类可以只使用超类功能的一部分。但这样的结果是:代码传达的信息与你的意图南辕北辙,你应该将它去除。
如果以委托取代继承,你可以更清晰地表明:你只需要受委托的一部分功能。接口中的哪一部分应该被使用,哪一部分应该被忽略,完全由你主导控制。这样做的成本则是需要额外写出委托函数,但这些函数都给出简单,极少可能出错。
12. Replace delegation with Inheritance 以继承代替委托
你在2个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数。让委托类继承受托类。
本项重构与Replace Inheritance with Delegation (以委托取代继承)恰恰相反。如果你发现自己需要受托类中的所有函数,并且花费很大力气编写所有极简单的委托函数,本重构可以帮助你轻松回头使用继承。
2条告诫需牢记与心:首先,如果你并没有使用受托类的所有函数,那么就不应该使用Replace Delegation with Inheritance (以继承取代委托),因为子类应该总是遵循超类的接口。如果过多的委托函数让你烦心,你有别的选择:你可以通过 Remove Middle Man (移除中间人)让客户端自己调用受托函数,也可以使用Extract Superclass (提炼超类)将2个类接口相同的部分提炼到超类中,然后让2个类都继承这个新的超类;你还可以用类似手法使用Extract Superclass (提炼超类)。
另一种需要当心的情况是:受托对象被不止一个其他对象共享,而且受托对象是可变的。在这种情况下,你就不能将委托关系替换为继承关系,因为这样就无法再共享数据了。数据共享是必须由受托对象承担的一种责任,你无法把它转给继承关系。如果受托对象是不可变的,数据共享就不成问题,因为你大可放心地复制对象。