本章将介绍几个能让你更轻松处理数据的重构手法。很多人或许会认为Self Encapsulate Fild(171)有点多余,但是关于"对象应该直接访问其中的数据,抑或应该通过访问函数来访问"这一问题,争论的声音从来不曾停止。有时候你确实需要访问函数,此时就可以通过SelfEncapsulate Field(171)得到它们。通常我会选择"直接访问"方式,因为我发现,只要我想做,任何时候进行这项重构都是很简单的。
面向对象语言有一个很有用的特征∶除了允许使用传统语言提供的简单数据类型,它们还允许你定义新类型。不过人们往往需要一段时间才能习惯这种编程方式。一开始你常会使用一个简单数值来表示某个概念。随着对系统的深入了解,你可能会明白,以对象表示这个概念,可能更合适。Replace Valuewth Object(175)让你可以将"哑"数据变成善表达的对象。如果你发现程序中有太多地方需要这一类对象,也可以使用Change Value to Reference(179)将它们变成引用对象。

  如果你看到一个数组的行为方式很像一个数据结构,就可以使用Replace Array withObject(186)把数组变成对象,从而使这个数据结构更清晰地显露出来。但这只是第一步,当你使用Move Method(142)为这个新对象加入相应行为时,真正的好处才得以体现。
魔法数———也就是带有特殊含义的数字———从来都是个问题。我还清楚记得,一开始学习编程的时候,老师就告诉我不要使用魔法数。但它们还是不时出现。因此,只要弄清楚魔法数的用途,我就运用 ReplaceMagicNumberwith Symbolic Constant (204)将它们除掉,以绝后患。

  对象之间的关联可以是单向的,也可以是双向的。单向关联比较简单,但有时为了支持一项新功能,你需要使用Change UnidirectionalAssociation to Bidirectional (197))将它变成双向关联。Change BidirecionalAssociation to Unidirectional(200)则恰恰相反∶如果你发现不再需要双向关联,可以使用这项重构将它变成单向关联。

  我常常遇到这样的情况∶GUI类竟然去处理不该它们处理的业务逻辑。为了把这些处理业务逻辑的行为移到合适的领域类去,你需要在领域类中保存这些逻辑的相关数据,并运用Duplicate ObservedData(189)提供对GUI的支持。一般来说,我不喜欢重复的数据,但这是一个例外,因为这里的重复数据通常是不可避免的。

  面向对象编程的关键原则之一就是封装。如果一个类公开了任何public数据,你就应该使用Encapsulate Field(206)将它郑重地包装起来。如果被公开的数据是个集合,就应该使用Encapsulate Collection(208),因为集合有其特殊协议。如果一整条记录都被裸露在外,就应该使用Replace Recordwith Data Class(217)。

  需要特别对待的一种数据是类型码(type code)∶这是一种特殊数值,用来指出"与实例所属之类型相关的某些东西"。类型码通常以枚举形式出现,并且通常以staticfinal整数实现。如果这些类型码用来表现某种信息,并且不会改变所属类型的行为,你可以运用Replace Type Code with Class((218)将它们替换掉,这项重构会为你提供更好的类型检查,以及一个更好的平台,使你可以在未来更方便地将相关行为添加进去。另一方面,如果当前类型的行为受到类型码的影响,你就应该尽可能使用ReplaceType CodewithSubclas(223)。如果做不到,就只好使用更复杂(同时也更灵活)的Replace Type Code with State/Srategy(227)。

8.1 Self EncapsulateField(自封装字段)

你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙
为了这个字段建立取值/设值函数,并且只以这些函数来访问字段.

动机

间接好处是,数据管理更加灵活,例如延迟初始化.
直接访问变量的好处则是:代码比较容易阅读.

做法

  1. 为待封装字段建立取值/设值函数
  2. 找出该字段的所有引用点,将他们全部改为调用取值/设值函数.
  3. 将该字段声明为private
  4. 复查,确保找出所有引用点.
  5. 编译,测试

8.2 Replace Data Value with Object(以对象取代数据值)

你有一个数据项,需要与其他数据和行为一起使用才有意义
将数据变成对象.

动机

  开发初期,你往往决定以简单的数据项表示简单的情况。但是,随着开发的进行,你可能会发现,这些简单数据项不再那么简单了。比如说,一开始你可能会用一个字符串来表示"电话号码"概念,但是随后你就会发现,电话号码需要"格式化"、"抽取区号"之类的特殊行为。如果这样的数据项只有一两个,你还可以把相关函数放进数据项所属的对象里;但是Duplicate Code坏味道和Feature Envy坏味道很快就会从代码中散发出来。当这些坏味道开始出现,你就应该将数据值变成对象。


做法

  1. 为待替换数值新建一个类,在其中声明一个final字段,其类型和源类中的带替换数值类型一样.然后在新类中加入这个字段的取值函数,再加上一个接受此字段为参数的构造函数.
  2. 编译
  3. 将源类中的待替换数值字段的类型改为前面新建的类.
  4. 修改源类中该字段的取值函数,令它调用新类的取值函数
  5. 如果源类构造函数中用到这个待替换字段(多半是赋值动作),我们就修改构造函数,令它改用新类的构造函数来对字段进行赋值动作.
  6. 修改源类中待替换字段的设值函数,令它为新类创建一个实例.
  7. 编译,测试.
  8. 现在,你有可能需要对新类型使用Change Value to Reference(179).

8.3 Change Value to Reference(将值对象改为引用对象)

你从一个类衍生出许多彼此相等的实例,希望将他们替换为同一个对象.
将这个值对象变成引用对象.

动机

  在许多系统中,你都可以对对象做一个有用的分类∶引用对象和值对象。前者就像"客户"、"账户"这样的东西,每个对象都代表真实世界中的一个实物,你可以直接以相等操作符(=,用来检验对象同一性)检查两个对象是否相等。后者则是像"日期"、"钱"这样的东西,它们完全由其所含的数据值来定义,你并不在意副本的存在,系统中或许存在成百上千个内容为"1/1/2000"的"日期"对象。当然,你也需要知道两个值对象是否相等,所以你需要覆写equal1s()(以及hashCode()。


  要在引用对象和值对象之间做选择有时并不容易。有时候,你会从一个简单的值对象开始,在其中保存少量不可修改的数据。而后,你可能会希望给这个对象加入一些可修改数据,并确保对任何一个对象的修改都能影响到所有引用此一对象的地方。这时候你就需要将这个对象变成一个引用对象。


做法

  1. 使用Replace Constructor with Factory Method(304).
  2. 编译,测试
  3. 决定由什么对象负责提供访问对象的途径.
  4. 决定这些引用对象应该预先创建好, 或是应该动态创建
  5. 修改工厂函数,令它返回引用对象.
  6. 编译,测试.

8.4 Change Reference to Value (将引用对象改为值对象)

你有一个引用对象,很小且不可变,且不易管理.
将他变成一个值对象.

动机

  正如我在Change Value to Reference(179)中所说,要在引用对象和值对象之间做选择,有时并不容易。作出选择后,你常会需要一条回头路。
  如果引用对象开始变得难以使用,也许就应该将它改为值对象。引用对象必须被某种方式控制,你总是必须向其控制者请求适当的引用对象。它们可能造成内存区域之间错综复杂的关联。在分布系统和并发系统中,不可变的值对象特别有用,因为你无需考虑它们的同步问题。
  值对象有一个非常重要的特性∶它们应该是不可变的。无论何时,只要你调用同一对象的同一个查询函数,都应该得到同样结果。如果保证了这一点,就可以放心地以多个对象表示同一个事物。如果值对象是可变的,你就必须确保对某一对象的修改会自动更新其他"代表相同事物"的对象。这太痛苦了,与其如此还不如把它变成引用对象。
  这里有必要澄清一下"不可变"(immutable)的意思。如果你以Money类表示"钱"的概念,其中有"币种"和"金额"两条信息,那么Money对象通常是一个不可变的值对象。这并非意味你的薪资不能改变,而是意味∶如果要改变你的薪资,就需要使用另一个Money对象来取代现有的Money对象,而不是在现有的Money对象上修改。你和Money对象之间的关系可以改变,但Money对象自身不能改变。


做法

  1. 检查重构目标是否为不可变对象,或是否可修改为不可变对象.
  2. 建立equals()和hashCode()
  3. 编译,测试.
  4. 考虑是否可以删除工厂函数,并将构造函数声明为public

8.5 Replace Array with Object(以对象取代数组)

你有一个数组,其中的元素各自代表不同的东西.
以对象替换数组.对于数组中的每个元素,以一个字段来表示.

动机

  数组是一种常见的用以组织数据的结构。不过,它们应该只用于"以某种顺序容纳一组相似对象"。有时候你会发现,一个数组容纳了多种不同对象,这会给用户带来麻烦,因为他们很难记住像"数组的第一个元素是人名"这样的约定。对象就不同了,你可以运用字段名称和函数名称来传达这样的信息,因此你无需死记它,也无需依赖注释。而且如果使用对象,你还可以将信息封装起来,并使用Move Method (142)为它加上相关行为。

做法

  1. 新建一个累标识数组所拥有的信息,并在其中以一个public 字段保存原先的数组。
  2. 修改数组的所有用户,让他们改用新类的实例
  3. 编译,测试
  4. 逐一为数组元素添加取值/设置函数。根据元素的用途,为这些访问函数命名。修改客户端代码,让他们通过访问函数取用数组内的元素,每次修改后,编译并测试
  5. 当所有对数组的直接访问都转而调用访问函数后,将新类中保存该数组的字段声明为private.
  6. 编译
  7. 对于数组内的每一个元素,在新类中创建一个类型相当的字段。修改该元素的访问函数,令它改用上述的新建字段
  8. 每修改一个元素,编译并测试。
  9. 数组的所有元素都有了响应字段之后,删除该数组

8.6 Duplicate Observed Data(复制"被监视数据")

你有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。
将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。

动机

  一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。之所以这样做,原因有以下几点∶(1)你可能需要使用不同的用户界面来表现相同的业务逻辑,如果同时承担两种责任,用户界面会变得过分复杂;(2)与GUI隔离之后,领域对象的维护和演化都会更容易,你甚至可以让不同的开发者负责不同部分的开发。
  尽管可以轻松地将"行为"划分到不同部位,"数据"却往往不能如此。同一项数据有可能既需要内嵌于GUI控件,也需要保存于领域模型里。自从MVC (Model-View-Controller,模型-视图-控制器)模式出现后,用户界面框架都使用多层系统来提供某种机制,使你不但可以提供这类数据,并保持它们同步。
  如果你遇到的代码是以两层方式开发,业务逻辑被内嵌于用户界面之中,你就有必要将行为分离出来。其中的主要工作就是函数的分解和搬移。但数据就不同了∶你不能仅仅只是移动数据,必须将它复制到新的对象中,并提供相应的同步机制。

做法

  1. 修改展现类,使其成为领域类的Observer
  2. 针对GUI类中的领域数据,使用SelfEncapsulate Field(171)
  3. 编译,测试
  4. 在时间处理函数中调用设值函数,直接更新GUI组件
  5. 编译,测试。
  6. 在领域类中定义数据及其相关访问函数。
  7. 修改展现类中的访问函数,将他们的操作对象改为领域对象(而非GUI组件)
  8. 修改Observer的Update(),使其从相应的领域对象中将所需数据复制给GUI组件。
  9. 编译,测试

使用事件监听器

  如果你使用事件监听器而不是Observer/Observable模式,仍然可以实施Duplicate ObservedData(189)。这种情况下,你需要在领域模型中建立一个监听器类和一个事件类(如果你不在意依赖关系的话,也可以使用AWT类)。然后,你需要对领域对象注册监听器,就像前例对observable对象注册observer一样。每当领域对象发生变化(类似上例的update()函数被调用),就向监听器发送一个事件。IntervalWindow可以利用一个内嵌类来实现监听器接口,并在适当时候调用适当的update()函数。

8.7 Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)

两个类都需要事用对方特性,但其间只有一条单向连接。
添加一个反向指针,并使修改函数能够同时更新两条连接

动机

  开发初期,你可能会在两个类之间建立一条单向连接,使其中一个类可以引用另一个类。随着时间推移,你可能发现被引用类需要得到其引用者以便进行某些处理。也就是说它需要一个反向指针。但指针是一种单向连接,你不可能反向操作它。通常你可以绕道而行,虽然会耗费一些计算时间,成本还算合理,然后你可以在被引用类中建立一个函数专门负责此一行为。但是,有时候想绕过这个问题并不容易,此时就需要建立双向引用关系,或称为反向指针。如果使用不当,反向指针很容易造成混乱;但只要你习惯了这种手法,它们其实并不是太复杂。

  "反向指针"手法有点棘手,所以在你能够自如运用之前,应该有相应的测试。通常我不花心思去测试访问函数,因为普通访问函数的风险没有高到需要测试的地步,但本重构要求测试访问函数,所以它是极少数需要添加测试的重构手法之一。

  本重构运用反向指针实现双向关联。其他技术(例如连接对象)需要其他重构手法。

做法

  1. 在被引用类中增加一个字段,用以保存反向指针。
  2. 决定由哪个类————引用端还是被引用端—————控制关联关系
  3. 在被控端建立一个辅助函数,其命名应该清楚支出他的有限用途.
  4. 如果既有的修改函数在控制端,让他负责更新反向指针.
  5. 如果机油的修改函数在被控制端,就在控制端建立一个控制函数,并让机油的修改函数调用这个新建的控制函数

8.8 Change Bidirectional Association To Unidirectional(将双向关联改为单向关联)

两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性.
去除不必要的关联.

动机

  双向关联很有用,但你也必须为它付出代价,那就是维护双向连接、确保对象被正确创建和删除而增加的复杂度。而且,由于很多程序员并不习惯使用双向关联,它往往成为错误之源。
大量的双向连接也很容易造成"僵尸对象"∶某个对象本来已经该死亡了,却仍然保留在系统中,因为对它的引用还没有完全清除。


  此外,双向关联也迫使两个类之间有了依赖∶对其中任一个类的任何修改,都可能引发另一个类的变化。如果这两个类位于不同的包,这种依赖就是包与包之间的相依。过多的跨包依赖会造就紧耦合系统,使得任何一点小小改动都可能造成许多无法预知的后果。


  只有在真正需要双向关联的时候,才应该使用它。如果发现双向关联不再有存在价值,就应该去掉其中不必要的一条关联。

做法

  1. 找出保存 你想去除的指针 的字段,检查他的每一个用户,判断是否可以去除该指针.
  2. 如果客户使用了取值函数,先运用Self Encapsulate Field 将待删除字段自我封装起来,然后使用 Substitute Algorithm 对付取值函数,令它不再使用该字段.然后编译,测试.
  3. 如果客户并未使用取值函数,那就直接修改待删除字段的所有被引用点:改以其他途径获得该字段所保存的对象.每次修改后,编译并测试.
  4. 如果已经没有任何函数使用待删除字段,移除所有对该字段的更新逻辑,然后移除该字段
  5. 编译,测试.

8.9 Replace Magic Number With Symbolic Constant (以字面常量取代魔法数)

你有一个字面数值,带有特别含义.
创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量.

动机


  在计算科学中,魔法数(magic number)是历史最悠久的不良现象之一。所谓魔法数是指拥有特殊意义,却又不能明确表现出这种意义的数字。如果你需要在不同的地点引用同一个逻辑数,魔法数会让你烦恼不已,因为一旦这些数发生改变,你就必须在程序中找到所有魔法数,并将它们全部修改一遍,这简直就是一场噩梦。就算你不需要修改,要准确指出每个魔法数的用途,也会让你颇费脑筋。


  许多语言都允许你声明常量。常量不会造成任何性能开销,却可以大大提高代码的可读性。


  进行本项重构之前,你应该先寻找其他替换方案。你应该观察魔法数如何被使用,而后你往往会发现一种更好的使用方式。如果这个魔法数是个类型码,请考虑使用Replace Type Code with Class(218);如果这个魔法数代表一个数组的长度,请在遍历该数组的时候,改用Array.length().

做法

  1. 声明一个常量,令其值为原本的魔法数值.
  2. 找出这个魔法数的所有引用点
  3. 检查是否可以使用这个新声明的常量连替换该魔法数.如果可以,便以此常量替换之.
  4. 编译
  5. 所有魔法数都被替换完毕后,编译并测试.此时整个程序应该运转如常,就像没有做任何修改一样.

8.10 Encapsulate Field(封装字段)

你的类中存在一个public字段.
将他声明为private,并提供相应的访问函数.

动机


  面向对象的首要原则之一就是封装,或者称为"数据隐藏"。按此原则,你绝不应该将数据声明为public,否则其他对象就有可能访问甚至修改这项数据,而拥有该数据的对象却毫无察觉。于是,数据和行为就被分开了———这可不是件好事。


  数据声明为public被看做是一种不好的做法,因为这样会降低程序的模块化程度。数据和使用该数据的行为如果集中在一起,一旦情况发生变化,代码的修改就会比较简单,因为需要修改的代码都集中于同一块地方,而不是星罗棋布地散落在整个程序中。


  EncapsulateField(206)是封装过程的第一步。通过这项重构手法,你可以将数据隐藏起来,并提供相应的访问函数。但它毕竟只是第一步。如果一个类除了访问函数外不能提供其他行为,它终究只是一个哑吧类。这样的类并不能享受对象技术带来的好处。而你知道,浪费任何一个对象都是很不好的。实施EncapstlateField((206)之后,我会尝试寻找用到新建访问函数的代码,看看是否可以通过简单的Move Method(142)轻快地将它们移到新对象去。


做法

  1. 为public字段提供取值/设值函数.
  2. 找到这个类以外使用该字段的所有地点,如果客户只是读取该字段,就把引用替换为对取值函数的调用,如果修改了,就替换为对设值函数的调用
  3. 每次修改之后,编译并测试
  4. 将此字段的所有用户修改完毕后,把字段声明为private
  5. 编译,测试

8.11 Encapsulate Collection(封装集合)

有个函数返回一个集合.
让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数.

动机


  我们常常会在一个类中使用集合(collection,可能是array、list、set或vector)来保存一组实例。这样的类通常也会提供针对该集合的取值/设值函数。



  但是,集合的处理方式应该和其他种类的数据略有不同。取值函数不该返回集合自身,因为这会让用户得以修改集合内容而集合拥有者却一无所悉。这也会对用户暴露过多对象内部数据结构的信息。如果一个取值函数确实需要返回多个值,它应该避免用户直接操作对象内所保存的集合,并隐藏对象内与用户无关的数据结构。至于如何做到这一点,视你使用的Java版本不同而有所不同。


  
另外,不应该为这整个集合提供一个设值函数,但应该提供用以为集合添加/移除元素的函数。这样,集合拥有者(对象)就可以控制集合元素的添加和移除。如果你做到以上几点,集合就被很好地封装起来了,这便可以降低集合拥有者和用户之间的耦合度。

做法

  1. 加入为集合添加/移除元素的函数
  2. 将保存集合的字段初始化为一个空集合.
  3. 编译
  4. 找出集合设置函数的所有调用者.你可以修改那个设置函数,让他使用上述新建立的 添加/移除元素 函数;也可以是直接修改调用端,让他们调用上述新建立的 函数.
  5. 编译,测试
  6. 找出所有 通过取值函数获得集合并修改其内容 的函数.逐一修改这些函数,让他们改用 添加/移除 函数.每次修改后,编译并测试
  7. 修改完上述所有 '通过取值函数获得集合并修改其内容' 的函数后,修改取值函数自身,使他返回该集合的一个只读副本.
  8. 编译,测试.
  9. 找出取值函数的所有用户,从中罩住应该存在于集合所属对象内的到吗.运用 Extract Method(110)和Move Method(142)将这些代码移到宿主对象去
  10. 修改现有取值函数的名字,然后添加一个新取值函数使其返回一个枚举,找出旧取值函数的所有被使用点,将他们都改为使用新取值函数.
  11. 如果这一步跨度太大,你可以先使用Rename Method(273)修改原取值函数的名称;再建立一个新取值函数用以返回枚举;最后在修改所有调用者,使其调用新取值函数.
  12. 编译,测试

将行为移到类中

封装数组

8.12 Replace Record with Data Class(以数据类取代记录)

你需要面对传统编程环境中的记录结构
为该记录创建一个 '哑' 数据对象

动机


  记录型结构是许多编程环境的共同性质。有一些理由使它们被带进面向对象程序之中∶你可能面对的是一个遗留程序,也可能需要通过一个传统API来与记录结构交流,或是处理从数据库读出的记录。这些时候你就有必要创建一个接口类,用以处理这些外来数据。最简单的做法就是先建立一个看起来类似外部记录的类,以便日后将某些字段和函数搬移到这个类之中。一个不太常见但非常令人注目的情况是∶数组中的每个位置上的元素都有特定含义,这种情况下应该使用ReplaceArray with Object(186)。


做法

  1. 新建一个类,表示这个记录
  2. 对记录中的每一项数据,在新建的类中建立对应的private字段,并提供相应的取值/设值函数

8.13 Replace Type Code with Class (以类取代类型码)

类之中有一个数值类型码,但它并不影响类的行为.
以一个新的类替换该数值类型码

动机


  在以C为基础的编程语言中,类型码或枚举值很常见。如果带着一个有意义的符号名,类型码的可读性还是不错的。问题在于,符号名终究只是个别名,编译器看见的、进行类型检验的,还是背后那个数值。任何接受类型码作为参数的函数,所期望的实际上是一个数值,无法强制使用符号名。这会大大降低代码的可读性,从而成为bug之源。



  如果把那样的数值换成一个类,编译器就可以对这个类进行类型检验。只要为这个类提供工厂函数,你就可以始终保证只有合法的实例才会被创建出来,而且它们都会被传递给正确的宿主对象。



  但是,在使用Replace Type Code with Class(218)之前,你应该先考虑类型码的其他替换方式。只有当类型码是纯粹数据时(也就是类型码不会在switch语句中引起行为变化时),你才能以类来取代它。Java只能以整数作为swicch语句的判断依据,不能使用任意类,因此那种情况下不能够以类替换类型码。更重要的是∶任何switch语句都应该运用Replace Conditional with Polymorphism(255)去掉。为了进行那样的重构,你首先必须运用Replace Tpe Codewih Subclsses(223)或Replace Tpe Code with Sate/Srategy(227),把类型码处理掉。


做法

  1. 为类型码建立一个类
  2. 修改源类实现,让它使用上述新建的类.
  3. 编译,测试.
  4. 对于源类中每一个使用类型码的函数,相应建立一个函数,让新函数使用新建的类.
  5. 逐一修改源类用户,让他们使用新接口
  6. 每修改一个用户,编译并测试.
  7. 删除使用类型码的旧接口,并删除保存旧类型码的静态变量.
  8. 编译,测试

8.14 Replace Type Code with Subclasses(以子类取代类型码)

你有一个不可变的类型码,他会影响类的行为.
子类取代这个类型码.

做法

  1. 使用 Self Encapsulate Field(171)将类型码自我封装起来
  2. 为类型码的每一个数值建立一个相应的子类.在每个子类中腹泻类型码的取值函数,使其返回相应的类型码值
  3. 没建立一个新的子类,编译并测试.
  4. 从超类中删掉保存类型码的字段.将类型码访问函数声明为抽象函数.
  5. 编译,测试

8.15 Replace Type Code with State/Strategy (以State/Strategy取代类型码)

你有一个类型码,他会影响类的行为,但你无法通过继承手法消除它
以状态对象取代类型码.

做法

  1. 使用 Self Encapsulate Field(171)将类型码自我封装起来
  2. 新建一个类,根据类型码的用途为它命名.这就是一个状态对象.
  3. 为这个新类添加子类,每个子类对应一种类型码
  4. 在超类中建立一个抽象的查询函数,用以返回类型码.在每个子类中腹泻该函数,返回确切的类型码
  5. 编译
  6. 在源类中建立一个字段,用以保存新建的状态对象
  7. 调整源类中负责查询类型码的函数,将查询动作转发给状态对象
  8. 调整源类中为类型码设值的函数,将一个恰当的状态对象子类赋值给保存状态对象的那个字段
  9. 编译测试

8.16 Replace Subclass with Fields(以字段取代子类)

你的各个子类的唯一差别只在'返回常量数据'的函数上.
修改这些函数,使他们返回超类中的某个字段,然后销毁子类

做法

  1. 对所有子类使用 Replace Constructor with Factory Method(304)
  2. 如果有任何代码直接引用子类,令它改变引用超类.
  3. 针对每个常量函数,在超类中声明一个final字段
  4. 为超类声明一个protected 构造函数,用以初始化这些新增字段
  5. 新建或修改子类构造函数,使它调用超类的新增构造函数
  6. 编译,测试
  7. 在超类中实现所有常量函数,令他们返回相应字段值,然后将该函数从子类中删掉.
  8. 没删除一个常量函数,编译并测试
  9. 子类中所有的常量函数都被删除后,使用Inline Method(117)将子类构造函数内联到超类的工厂函数中
  10. 编译,测试
  11. 将子类删掉
  12. 编译,测试
  13. 重复'内联构造函数,删除子类'过长,知道所有子类都被删除