学习重构(4)-重新组织数据
1. Self Encapsulate Field(自封装值域)
你直接访问一个值域,但与值域之间的耦合关系逐渐变得笨拙。为这个值域建立取值/设值函数(get/set),并且只以这些函数来访问值域。
应用场景:如果你想访问superclass中的一个值域,却又想在subclass中将[对这个变量的访问]改为一个计算后的值,这就是最该使用Self Encapsulate Field(171)的时候。[值域自我封装]只是第一步。完成自我封装之后,你可以在subclass中根据自己的需要随意覆写取值/设值函数(getting/setting methods)。
示例:
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};
2. Replace Data Value with Object(以对象取代数据值)
将一些数据按照一定的规则或特征整合到一个对象中
应用场景:开发代码初期,仅需要一些简单的数据,比如一个人的基本信息:姓名,身份证号码,随着时间的推移,还需要其他的数据:手机号,地址,学历等等,这样就会罗列一排的变量,每次处理一个人的数据是,要传入很多参数。此时将这个人的信息就可以规整到一个类里面,仅声明一个对象即可,而且随着这个人信息的持续丰富,不会每次都冲击到原有的接口。
3. Change Value to Reference(将实值对象改为引用对象)
你有一个class,衍生出许多相等的实体,你希望将他们替换为单对象。将这个实值对象变成一个引用对象。
应用场景:我们一般使用一个类的时候都是需要了就new一个对象出来,这些每次new出来的对象就是实值对象,但很多场景下我们会改变这些对象的内部状态,并且需要对这些对象进行传递,此时用实值对象来满足这个诉求就会显得非常笨拙,比如new出来一个car,然后设置下品牌是宝马,设置下颜色是蓝色等等。这种场景下我们就需要一个引用对象,同一个类中可以是成员变量,不同的类中使用可以使用类似工厂、单例等模式来有一个对象管理类来供其他类取用。
4. Change Reference to Value(将引用对象改为实值对象)
和3相反。
应用场景:实值对象一个重要特点是保持不变,这样可以保证每个使用者不用担心数据的变化和同步。引用对象往往会让逻辑变得复杂,所以使用的时候要做好选择。
5. 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");
6. Duplicate Observed Data(赋值“被监视数据”)
你有一笔domain data置身于GUI空间中,而domain method需要访问它们。将这笔数据拷贝到一个domain object中,建立一个Observer模式,用意对domain object和GUI object内的重复数据进行同步控制。
应用场景:一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。之所以这样做,原因有一下几点:(1)你可能需要使用不同的用户界面来表现相同的业务逻辑,如果同时承担两种责任,用户界面会变得过分复杂;(2)与GUI隔离后,领域对象的维护和演化都会更容易,甚至可以让不同的开发者负责不同部分的开发。尽管可以轻松地将“行为”划分到不同部位,“数据”却往往不能如此。同一项数据有可能既需要内嵌于GUI控件,也需要保存于领域模型里。自从MVC模式出现后,用户界面框架都是用多层系统来提供某种机制,使你不但可以提供这类数据,并保持他们同步。如果代码是以两层方式开发,业务逻辑被内嵌于用户界面之中,就有必要将行为分离出来。其中的主要工作就是函数的分解和搬移。但数据就不同了:不能仅仅只是移动数据,必须将它复制到新对象中,并提供相应的同步机制。
7. Change Unidirectional Association to Bidirectional(将单项关联改为双向)
两个classes都需要使用对方特性,但其间只有一条单向连接。添加一个反向指针,并使修改函数能够同时更新两条连接。
应用场景:开发初期,你可能会在两个classes之间建立一条单向连接,使其中一个class可以引用另一个class。随着时间的推移,你可能发现被引用的class需要得到其引用者来进行某些处理,也就是说它需要一个反向指针。
8. Change Bidirectional Association to Unidirectional(将双向关联改为单向)
应用场景:双向关联很有用,但你也必须为它付出代价,那就是“维护双向连接,确保对象被正确的创建和删除”而增加的复杂度。所以只有在你需要双向关联的时候,才应该使用它。如果你发现双向关联不再有存在价值,就应该去掉其中不必要的一条关联。
9. Replace Magic Number with Symbolic Constant(以符号常量/字面常量取代魔法数字)
应用场景:在计算科学中,魔法数(magic number)是历史最悠久的不良现象之一。所谓魔法数是指拥有特殊意义,却又不能明确表现出这种意义的数字。如果你需要在不同的地点引用同一个逻辑数,魔法数会让你烦恼不已,因为一旦这些数发生改变,你就必须在程序中找到所有魔法数,并将它们全部修改一遍,这简直就是一场噩梦。就 算你不需要修改,要准确指出每个魔法数的用途,也会让你颇费脑筋。许多语言都允许你声明常量。常量不会造成任何性能开销,却可以大大提高代码的可读性。进行本项重构之前,你应该先寻找其他替换方案。你应该观察魔法数如何被使用,而后往往你会发现一种更好的使用方式。如果这个魔法数是个type code(型别码), 请考虑使用Replace Type Code with Class;如果这个魔法数代表一个数组的长度,请在遍历该数组的时候,改用Array.length()。
10. Encapsulate Field(封装值域)
能理解和第一条“自封装值域”的区别吗?
应用场景:面向对象的首要原则之一就是封装(encapsulation),或者称为「数据隐藏」(data hidding)。按此原则,你绝不应该将数据声明为public,否则其他对象就有可能访问甚至修改这项数据,而拥有该数据的对象却毫无察觉。这就将数据和行为分开了(不妙)。public 数据被看做是一种不好的作法,因为这样会降低程序的模块化程度(modularity)。如果数据和使用该数据的行为被集中在一起,一旦情况发生变化,代码的修改就会比较简单,因为需要修改的代码都集中于同一块地方,而不是星罗棋布地散落在整个程序中。Encapsulate Field是封装过程的第一步。通过这项重构手法,你可以将数据隐藏起来,并提供相应的访问函数(accessors)。但它毕竟只是第一步。如果一个class除了访问函数(accessors)外不能提供其他行为,它终究只是一个dumb class (哑类〕。这样的class并不能获得对象技术的优势,而你知道,浪费任何一个对象都是很不好的。实施Encapsulate Field之后,我会尝试寻找那些使用「新建 访问函数」的函数,看看是否可以通过简单的Move Method 轻快地将它们移到新对象去。
11. Encapsulate Collection(封装群集)
应用场景:class常常会使用群集(collection,可能是array、list、set或vector)来保存一组实体。这样的class通常也会提供针对该群集的「取值/设值函数」(getter/setter)。但是,群集的处理方式应该和其他种类的数据略有不同。取值函数(getter)不该返回群集自身,因为这将让用户得以修改群集内容而群集拥有者却一无所悉。这也会对用户暴露过多「对象内部数据结构」的信息。如果一个取值函数(getter)确实需要返回多个值,它应该避免用户直接操作对象内所保存的群集,并隐藏对象内「与用户无关」的数据结构。至于如何做到这一点,视你使用的版本不同而有所不同。另外,不应该为这整个群集提供一个设值函数(setter),但应该提供用以为群集添加/移除(add/remove)元素的函数。这样,群集拥有者(对象)就可以控制群集元素的添加和移除。如果你做到以上数点,群集(collection)就被很好地封装起来了,这便可以降低群集拥有者(class)和用户之间的耦合度。
12. Replace Record with Data Class(以数据类取代记录)
应用场景:Record structures (记录型结构)是许多编程环境的共同性质。有一些理由使它们被带进面向对象程序之中:你可能面对的是一个老旧程序( legacy program ),也可能需要通过一个传统API 来与structured record 交流,或是处理从数据库读出的 records。这些时候你就有必要创建一个interfacing class ,用以处理这些外来数据。最简单的作法就是先建立一个看起来类似外部记录(external record)的class ,以便日后将某些值域和函数搬移到这个class 之中。一个不太常见但非常令人注目的情况是:数组中的每个位置上的元素都有特定含义,这种情况下你应该使用Replace Array with Object 。
13. Replace Type Code with Class(以类取代型别码)
应用场景:在以C 为基础的编程语言中,type code(型别码)或枚举值(enumerations)很常见。如果带着一个有意义的符号名,type code 的可读性还是不错的。问题在于,符号名终究只是个别名,编译器看见的、进行型别检验的,还是背后那个数值。任何接受type code 作为引数(argument)的函数,所期望的实际上是一个数值,无法强制使用符号名。这会大大降低代码的可读性,从而成为臭虫之源。如果把那样的数值换成一个class ,编译器就可以对这个class 进行型别检验。只要为这个class 提供factory methods ,你就可以始终保证只有合法的实体才会被创建出 来,而且它们都会被传递给正确的宿主对象。但是,在使用Replace Type Code with Class 之前,你应该先考虑type code 的其他替换方式。只有当type code 是纯粹数据时(也就是type code 不会在switch 语句中引起行为变化时),你才能以class 来取代它。Java 只能以整数作为switch 语句的「转辙」依据,不能使用任意class ,因此那种情况下不能够以class 替换type code 。更重要的是:任何switch 语句都应该运用 Replace Conditional with Polymorphism 去掉。为了进行那样的重构,你首先必须运用 Replace Type Code with Subclasses 或Replace Type Code with State/Strategy 把type code处理掉。即使一个type code 不会因其数值的不同而引起行为上的差异,宿主类中的某些行为还是有可能更适合置放于type code class 中,因此你还应该留意是否有必要使用Move Method 将一两个函数搬过去。
14. Replace Type Code with Subclasses(以子类取代型别码)
应用场景:如果你面对的type code 不会影响宿主类的行为,你可以使用Replace Type Code with Class 来处理它们。但如果type code 会影响宿主类的行为,那么最好的办法就是借助多态(polymorphism )来处理变化行为。一般来说,这种情况的标志就是像switch 这样的条件式。这种条件式可能有两种表现形式:switch 语句或者if-then-else 结构。不论哪种形式,它们都是检查type code 值,并根据不同的值执行不同的动作。这种情况下你应该以Replace Conditional with Polymorphism 进行重构。但为了能够顺利进行那样的重构,首先应该将type code 替换为可拥有多态行为的继承体系。这样的一个继承体系应该以type code 的宿主类为base class,并针对每一种type code 各建立一个subclass 。为建立这样的继承体系,最简单的办法就是Replace Type Code with Subclasses:以type code 的宿主类为base class,针对每种type code 建立相应的subclass 。 但是以下两种情况你不能那么做:(1) type code 值在对象创建之后发生了改变;(2) 由于某些原因,type code 宿主类已经有了subclass 。如果你恰好面临这两种情况之一,就需要使用Replace Type Code with State/Strategy 。Replace Type Code with Subclasses 的主要作用其实是搭建一个舞台,让Replace Conditional with Polymorphism 得以一展身手。如果宿主类中并没有出现条件式,那么 Replace Type Code with Class 更合适,风险也比较低。使用Replace Type Code with Subclasses 的另一个原因就是,宿主类中出现 了「只与具备特定type code 之对象相关」的特性。完成本项重构之后,你可以使用 Push Down Method 和 Push Down Field 将这些特性推到合适的subclass去,以彰显它们「只与特定情况相关」这一事实。Replace Type Code with Subclasses 的好处在于:它把「对不同行为的了解」从class 用户那儿转移到了class 自身。如果需要再加入新的行为变化,我只需添加subclass 一个就行了。如果没有多态机制,我就必须找到所有条件式,并逐一修改它们。因此,如果未来还有可能加入新行为,这项重构将特别有价值。
15. Replace Type Code with State/Strategy(以State/strategy 取代型别码)
应用场景:本项重构和Replace Type Code with Subclasses 很相似,但如果「type code 的值在对象生命期中发生变化」或「其他原因使得宿主类不能被subclassing 」,你也可以使用本重构。本重构使用State 模式或Stategy 模式[Gang of Four]。State 模式和Stategy 模式非常相似,因此无论你选择其中哪一个,重构过程都是相同的。「选择哪一个模式」并非问题关键所在,你只需要选择更适合特定情境的模式就行了。如果你打算在完成本项重构之后再以 Replace Conditional with Polymorphism 简化一个算法,那么选择Stategy 模式比较合适;如果你打算搬移与状态相关(state-specific)的数据,而且你把新建对象视为一种变迁状态 (changing state),就应该选择使用State 模式。
16. Replace Subclass with Fields(以值域取代子类)
应用场景:建立subclass 的目的,是为了增如新特性,或变化其行为。有一种变化行为(variant behavior )称为「常量函数」(constant method)[Beck],它们会返回一个硬编码 (hard-coded)值。这东西有其用途:你可以让不同的subclasses 中的同一个访问函数(accessors)返回不同的值。你可以在superclass 中将访问函数声明为抽象函数, 并在不同的subclass 中让它返回不同的值。尽管常量函数有其用途,但若subclass 中只有常量函数,实在没有足够的存在价值。 你可以在中设计一个与「常量函数返回值」相应的值域,从而完全去除这样的subclass 。如此一来就可以避免因subclassing 而带来的额外复杂性。